Skip to content

Commit 343594e

Browse files
Exempt parameters resolved from DI from validation (dotnet#61895)
* Exempt parameters resolved from DI from validation * Update src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs Co-authored-by: Copilot <[email protected]> * Add back brace * Fix up handling for keyed services --------- Co-authored-by: Copilot <[email protected]>
1 parent f544e7c commit 343594e

File tree

6 files changed

+93
-2
lines changed

6 files changed

+93
-2
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,18 @@ internal static bool IsExemptType(this ITypeSymbol type, WellKnownTypes wellKnow
124124

125125
return null;
126126
}
127+
128+
/// <summary>
129+
/// Checks if the parameter is marked with [FromService] or [FromKeyedService] attributes.
130+
/// </summary>
131+
/// <param name="parameter">The parameter to check.</param>
132+
/// <param name="fromServiceMetadataSymbol">The symbol representing the [FromService] attribute.</param>
133+
/// <param name="fromKeyedServiceAttributeSymbol">The symbol representing the [FromKeyedService] attribute.</param>
134+
internal static bool IsServiceParameter(this IParameterSymbol parameter, INamedTypeSymbol fromServiceMetadataSymbol, INamedTypeSymbol fromKeyedServiceAttributeSymbol)
135+
{
136+
return parameter.GetAttributes().Any(attr =>
137+
attr.AttributeClass is not null &&
138+
(attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) ||
139+
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol)));
140+
}
127141
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,23 @@ internal ImmutableArray<ValidatableType> ExtractValidatableTypes(IInvocationOper
2525
var parameters = operation.TryGetRouteHandlerMethod(operation.SemanticModel, out var method)
2626
? method.Parameters
2727
: [];
28+
29+
var fromServiceMetadataSymbol = wellKnownTypes.Get(
30+
WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata);
31+
var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get(
32+
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
33+
2834
var validatableTypes = new HashSet<ValidatableType>(ValidatableTypeComparer.Instance);
2935
List<ITypeSymbol> visitedTypes = [];
36+
3037
foreach (var parameter in parameters)
3138
{
39+
// Skip parameters that are injected as services
40+
if (parameter.IsServiceParameter(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol))
41+
{
42+
continue;
43+
}
44+
3245
_ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
3346
}
3447
return [.. validatableTypes];

src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,23 @@ public async Task CanValidateIValidatableObject()
1515
using Microsoft.AspNetCore.Builder;
1616
using Microsoft.AspNetCore.Http;
1717
using Microsoft.AspNetCore.Http.Validation;
18+
using Microsoft.AspNetCore.Mvc;
1819
using Microsoft.AspNetCore.Routing;
1920
using Microsoft.Extensions.DependencyInjection;
2021
2122
var builder = WebApplication.CreateBuilder();
2223
builder.Services.AddSingleton<IRangeService, RangeService>();
24+
builder.Services.AddKeyedSingleton<TestService>("serviceKey");
2325
builder.Services.AddValidation();
2426
2527
var app = builder.Build();
2628
27-
app.MapPost("/validatable-object", (ComplexValidatableType model) => Results.Ok());
29+
app.MapPost("/validatable-object", (
30+
ComplexValidatableType model,
31+
// Demonstrates that parameters that are annotated with [FromService] are not processed
32+
// by the source generator and not emitted as ValidatableTypes in the generated code.
33+
[FromServices] IRangeService rangeService,
34+
[FromKeyedServices("serviceKey")] TestService testService) => Results.Ok(rangeService.GetMinimum()));
2835
2936
app.Run();
3037
@@ -86,6 +93,12 @@ public class RangeService : IRangeService
8693
public int GetMinimum() => 10;
8794
public int GetMaximum() => 100;
8895
}
96+
97+
public class TestService
98+
{
99+
[Range(10, 100)]
100+
public int Value { get; set; } = 4;
101+
}
89102
""";
90103

91104
await Verify(source, out var compilation);

src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,43 @@ public async Task CanValidateParameters()
1616
using Microsoft.AspNetCore.Builder;
1717
using Microsoft.AspNetCore.Http;
1818
using Microsoft.AspNetCore.Http.Validation;
19+
using Microsoft.AspNetCore.Mvc;
1920
using Microsoft.AspNetCore.Routing;
2021
using Microsoft.Extensions.DependencyInjection;
2122
2223
var builder = WebApplication.CreateBuilder();
2324
2425
builder.Services.AddValidation();
26+
builder.Services.AddSingleton<TestService>();
27+
builder.Services.AddKeyedSingleton<TestService>("serviceKey");
2528
2629
var app = builder.Build();
2730
2831
app.MapGet("/params", (
32+
// Skipped from validation because it is resolved as a service by IServiceProviderIsService
33+
TestService testService,
34+
// Skipped from validation because it is marked as a [FromKeyedService] parameter
35+
[FromKeyedServices("serviceKey")] TestService testService2,
2936
[Range(10, 100)] int value1,
3037
[Range(10, 100), Display(Name = "Valid identifier")] int value2,
3138
[Required] string value3 = "some-value",
3239
[CustomValidation(ErrorMessage = "Value must be an even number")] int value4 = 4,
33-
[CustomValidation, Range(10, 100)] int value5 = 10) => "OK");
40+
[CustomValidation, Range(10, 100)] int value5 = 10,
41+
// Skipped from validation because it is marked as a [FromService] parameter
42+
[FromServices] [Range(10, 100)] int? value6 = 4) => "OK");
3443
3544
app.Run();
3645
3746
public class CustomValidationAttribute : ValidationAttribute
3847
{
3948
public override bool IsValid(object? value) => value is int number && number % 2 == 0;
4049
}
50+
51+
public class TestService
52+
{
53+
[Range(10, 100)]
54+
public int Value { get; set; } = 4;
55+
}
4156
""";
4257
await Verify(source, out var compilation);
4358
await VerifyEndpoint(compilation, "/params", async (endpoint, serviceProvider) =>

src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public GeneratedValidatableTypeInfo(
6262
public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo)
6363
{
6464
validatableInfo = null;
65+
if (type == typeof(global::TestService))
66+
{
67+
validatableInfo = CreateTestService();
68+
return true;
69+
}
6570

6671
return false;
6772
}
@@ -73,6 +78,20 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn
7378
return false;
7479
}
7580

81+
private ValidatableTypeInfo CreateTestService()
82+
{
83+
return new GeneratedValidatableTypeInfo(
84+
type: typeof(global::TestService),
85+
members: [
86+
new GeneratedValidatablePropertyInfo(
87+
containingType: typeof(global::TestService),
88+
propertyType: typeof(int),
89+
name: "Value",
90+
displayName: "Value"
91+
),
92+
]
93+
);
94+
}
7695

7796
}
7897

src/Http/Routing/src/ValidationEndpointFilterFactory.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
// The .NET Foundation licenses this file to you under the MIT license.
55

66
using System.ComponentModel.DataAnnotations;
7+
using System.Linq;
78
using System.Reflection;
9+
using Microsoft.AspNetCore.Http.Metadata;
810
using Microsoft.Extensions.DependencyInjection;
911
using Microsoft.Extensions.Options;
1012

@@ -21,13 +23,21 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context
2123
return next;
2224
}
2325

26+
var serviceProviderIsService = context.ApplicationServices.GetService<IServiceProviderIsService>();
27+
2428
var parameterCount = parameters.Length;
2529
var validatableParameters = new IValidatableInfo[parameterCount];
2630
var parameterDisplayNames = new string[parameterCount];
2731
var hasValidatableParameters = false;
2832

2933
for (var i = 0; i < parameterCount; i++)
3034
{
35+
// Ignore parameters that are resolved from the DI container.
36+
if (IsServiceParameter(parameters[i], serviceProviderIsService))
37+
{
38+
continue;
39+
}
40+
3141
if (options.TryGetValidatableParameterInfo(parameters[i], out var validatableParameter))
3242
{
3343
validatableParameters[i] = validatableParameter;
@@ -85,6 +95,13 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context
8595
};
8696
}
8797

98+
private static bool IsServiceParameter(ParameterInfo parameterInfo, IServiceProviderIsService? isService)
99+
=> HasFromServicesAttribute(parameterInfo) ||
100+
(isService?.IsService(parameterInfo.ParameterType) == true);
101+
102+
private static bool HasFromServicesAttribute(ParameterInfo parameterInfo)
103+
=> parameterInfo.CustomAttributes.OfType<IFromServiceMetadata>().Any();
104+
88105
private static string GetDisplayName(ParameterInfo parameterInfo)
89106
{
90107
var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>();

0 commit comments

Comments
 (0)