Skip to content

Commit c063ce5

Browse files
committed
Exempt more types, add no-op tests
1 parent 21c2418 commit c063ce5

File tree

6 files changed

+272
-5
lines changed

6 files changed

+272
-5
lines changed

src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
5959
"Consider increasing the MaxDepth in ValidationOptions if deeper validation is required.");
6060
}
6161

62-
// Increment depth counter since we're coming from
63-
// a parameter or property reference
6462
var originalPrefix = context.CurrentValidationPath;
6563

6664
try

src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,18 @@ internal static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol inte
8787
return derivedTypes.Count == 0 ? null : derivedTypes.ToImmutable();
8888
}
8989

90+
// Types exempted here have special binding rules in RDF and RDG and are not validatable
91+
// types themselves so we short-circuit on them.
9092
internal static bool IsExemptType(this ITypeSymbol type, RequiredSymbols requiredSymbols)
9193
{
9294
return SymbolEqualityComparer.Default.Equals(type, requiredSymbols.HttpContext)
9395
|| SymbolEqualityComparer.Default.Equals(type, requiredSymbols.HttpRequest)
94-
|| SymbolEqualityComparer.Default.Equals(type, requiredSymbols.HttpResponse);
96+
|| SymbolEqualityComparer.Default.Equals(type, requiredSymbols.HttpResponse)
97+
|| SymbolEqualityComparer.Default.Equals(type, requiredSymbols.CancellationToken)
98+
|| SymbolEqualityComparer.Default.Equals(type, requiredSymbols.IFormCollection)
99+
|| SymbolEqualityComparer.Default.Equals(type, requiredSymbols.IFormFileCollection)
100+
|| SymbolEqualityComparer.Default.Equals(type, requiredSymbols.IFormFile)
101+
|| SymbolEqualityComparer.Default.Equals(type, requiredSymbols.Stream)
102+
|| SymbolEqualityComparer.Default.Equals(type, requiredSymbols.PipeReader);
95103
}
96104
}

src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/RequiredSymbols.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,11 @@ internal sealed record class RequiredSymbols(
1515
INamedTypeSymbol CustomValidationAttribute,
1616
INamedTypeSymbol HttpContext,
1717
INamedTypeSymbol HttpRequest,
18-
INamedTypeSymbol HttpResponse
18+
INamedTypeSymbol HttpResponse,
19+
INamedTypeSymbol CancellationToken,
20+
INamedTypeSymbol IFormCollection,
21+
INamedTypeSymbol IFormFileCollection,
22+
INamedTypeSymbol IFormFile,
23+
INamedTypeSymbol Stream,
24+
INamedTypeSymbol PipeReader
1925
);

src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.RequiredSymbolsParser.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ internal RequiredSymbols ExtractRequiredSymbols(Compilation compilation, Cancell
2020
compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.CustomValidationAttribute")!,
2121
compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.HttpContext")!,
2222
compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.HttpRequest")!,
23-
compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.HttpResponse")!
23+
compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.HttpResponse")!,
24+
compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!,
25+
compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.IFormCollection")!,
26+
compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.IFormFileCollection")!,
27+
compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.IFormFile")!,
28+
compilation.GetTypeByMetadataName("System.IO.Stream")!,
29+
compilation.GetTypeByMetadataName("System.IO.Pipelines.PipeReader")!
2430
);
2531
}
2632
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests;
5+
6+
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
7+
{
8+
[Fact]
9+
public async Task DoesNotEmitIfNoAddValidationCallExists()
10+
{
11+
// Arrange
12+
var source = """
13+
using System;
14+
using System.ComponentModel.DataAnnotations;
15+
using System.Collections.Generic;
16+
using System.Threading.Tasks;
17+
using Microsoft.AspNetCore.Builder;
18+
using Microsoft.AspNetCore.Http;
19+
using Microsoft.AspNetCore.Http.Validation;
20+
using Microsoft.AspNetCore.Routing;
21+
using Microsoft.Extensions.DependencyInjection;
22+
23+
var builder = WebApplication.CreateBuilder();
24+
25+
var app = builder.Build();
26+
27+
app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed"));
28+
29+
app.Run();
30+
31+
public class ComplexType
32+
{
33+
[Range(10, 100)]
34+
public int IntegerWithRange { get; set; } = 10;
35+
}
36+
""";
37+
await Verify(source, out var compilation);
38+
// Verify that we don't validate types if no AddValidation call exists
39+
await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) =>
40+
{
41+
var payload = """
42+
{
43+
"IntegerWithRange": 5
44+
}
45+
""";
46+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
47+
48+
await endpoint.RequestDelegate(context);
49+
50+
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
51+
});
52+
}
53+
54+
[Fact]
55+
public async Task DoesNotEmitForExemptTypes()
56+
{
57+
var source = """
58+
using System;
59+
using System.ComponentModel.DataAnnotations;
60+
using System.IO;
61+
using System.IO.Pipelines;
62+
using System.Threading;
63+
using Microsoft.AspNetCore.Builder;
64+
using Microsoft.AspNetCore.Http;
65+
using Microsoft.AspNetCore.Http.Validation;
66+
using Microsoft.Extensions.DependencyInjection;
67+
68+
var builder = WebApplication.CreateBuilder();
69+
70+
builder.Services.AddValidation();
71+
72+
var app = builder.Build();
73+
74+
app.MapGet("/exempt-1", (HttpContext context) => Results.Ok("Exempt Passed!"));
75+
app.MapGet("/exempt-2", (HttpRequest request) => Results.Ok("Exempt Passed!"));
76+
app.MapGet("/exempt-3", (HttpResponse response) => Results.Ok("Exempt Passed!"));
77+
app.MapGet("/exempt-4", (IFormCollection formCollection) => Results.Ok("Exempt Passed!"));
78+
app.MapGet("/exempt-5", (IFormFileCollection formFileCollection) => Results.Ok("Exempt Passed!"));
79+
app.MapGet("/exempt-6", (IFormFile formFile) => Results.Ok("Exempt Passed!"));
80+
app.MapGet("/exempt-7", (Stream stream) => Results.Ok("Exempt Passed!"));
81+
app.MapGet("/exempt-8", (PipeReader pipeReader) => Results.Ok("Exempt Passed!"));
82+
app.MapGet("/exempt-9", (CancellationToken cancellationToken) => Results.Ok("Exempt Passed!"));
83+
app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed"));
84+
85+
app.Run();
86+
87+
public class ComplexType
88+
{
89+
[Range(10, 100)]
90+
public int IntegerWithRange { get; set; } = 10;
91+
}
92+
""";
93+
await Verify(source, out var compilation);
94+
// Verify that we can validate non-exempt types
95+
await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) =>
96+
{
97+
var payload = """
98+
{
99+
"IntegerWithRange": 5
100+
}
101+
""";
102+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
103+
104+
await endpoint.RequestDelegate(context);
105+
106+
var problemDetails = await AssertBadRequest(context);
107+
Assert.Collection(problemDetails.Errors, kvp =>
108+
{
109+
Assert.Equal("IntegerWithRange", kvp.Key);
110+
Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
111+
});
112+
});
113+
}
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//HintName: ValidatableInfoResolver.g.cs
2+
#nullable enable annotations
3+
//------------------------------------------------------------------------------
4+
// <auto-generated>
5+
// This code was generated by a tool.
6+
//
7+
// Changes to this file may cause incorrect behavior and will be lost if
8+
// the code is regenerated.
9+
// </auto-generated>
10+
//------------------------------------------------------------------------------
11+
#nullable enable
12+
13+
namespace System.Runtime.CompilerServices
14+
{
15+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
16+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
17+
file sealed class InterceptsLocationAttribute : System.Attribute
18+
{
19+
public InterceptsLocationAttribute(int version, string data)
20+
{
21+
}
22+
}
23+
}
24+
25+
namespace Microsoft.AspNetCore.Http.Validation.Generated
26+
{
27+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
28+
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
29+
{
30+
public GeneratedValidatablePropertyInfo(
31+
global::System.Type containingType,
32+
global::System.Type propertyType,
33+
string name,
34+
string displayName) : base(containingType, propertyType, name, displayName)
35+
{
36+
ContainingType = containingType;
37+
Name = name;
38+
}
39+
40+
internal global::System.Type ContainingType { get; }
41+
internal string Name { get; }
42+
43+
protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
44+
=> ValidationAttributeCache.GetValidationAttributes(ContainingType, Name);
45+
}
46+
47+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
48+
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
49+
{
50+
public GeneratedValidatableTypeInfo(
51+
global::System.Type type,
52+
ValidatablePropertyInfo[] members) : base(type, members) { }
53+
}
54+
55+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
56+
file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver
57+
{
58+
public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo)
59+
{
60+
validatableInfo = null;
61+
if (type == typeof(global::ComplexType))
62+
{
63+
validatableInfo = CreateComplexType();
64+
return true;
65+
}
66+
67+
return false;
68+
}
69+
70+
// No-ops, rely on runtime code for ParameterInfo-based resolution
71+
public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo)
72+
{
73+
validatableInfo = null;
74+
return false;
75+
}
76+
77+
private ValidatableTypeInfo CreateComplexType()
78+
{
79+
return new GeneratedValidatableTypeInfo(
80+
type: typeof(global::ComplexType),
81+
members: [
82+
new GeneratedValidatablePropertyInfo(
83+
containingType: typeof(global::ComplexType),
84+
propertyType: typeof(int),
85+
name: "IntegerWithRange",
86+
displayName: "IntegerWithRange"
87+
),
88+
]
89+
);
90+
}
91+
92+
}
93+
94+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
95+
file static class GeneratedServiceCollectionExtensions
96+
{
97+
[InterceptsLocation]
98+
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action<ValidationOptions>? configureOptions = null)
99+
{
100+
// Use non-extension method to avoid infinite recursion.
101+
return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options =>
102+
{
103+
options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver());
104+
if (configureOptions is not null)
105+
{
106+
configureOptions(options);
107+
}
108+
});
109+
}
110+
}
111+
112+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
113+
file static class ValidationAttributeCache
114+
{
115+
private sealed record CacheKey(global::System.Type ContainingType, string PropertyName);
116+
private static readonly global::System.Collections.Concurrent.ConcurrentDictionary<CacheKey, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> _cache = new();
117+
118+
public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
119+
global::System.Type containingType,
120+
string propertyName)
121+
{
122+
var key = new CacheKey(containingType, propertyName);
123+
return _cache.GetOrAdd(key, static k =>
124+
{
125+
var property = k.ContainingType.GetProperty(k.PropertyName);
126+
if (property == null)
127+
{
128+
return [];
129+
}
130+
131+
return [.. global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true)];
132+
});
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)