From 0a227a622fafef3e31b83362bab5aa8cc70f5fef Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 12 May 2025 17:37:42 -0700 Subject: [PATCH 1/4] Exempt parameters resolved from DI from validation --- .../ValidationsGenerator.TypesParser.cs | 10 ++++++++++ ...ValidationsGenerator.IValidatableObject.cs | 7 ++++++- .../ValidationsGenerator.Parameters.cs | 14 +++++++++++++- ...ters#ValidatableInfoResolver.g.verified.cs | 19 +++++++++++++++++++ .../src/ValidationEndpointFilterFactory.cs | 17 +++++++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs index dd0092f31fd0..dc8f7463e672 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs @@ -25,10 +25,20 @@ internal ImmutableArray ExtractValidatableTypes(IInvocationOper var parameters = operation.TryGetRouteHandlerMethod(operation.SemanticModel, out var method) ? method.Parameters : []; + + var fromServiceMetadataSymbol = wellKnownTypes.Get( + WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata); + var validatableTypes = new HashSet(ValidatableTypeComparer.Instance); List visitedTypes = []; + foreach (var parameter in parameters) { + if (parameter.GetAttributes().Any(attr => attr.AttributeClass is not null && attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol))) + { + continue; + } + _ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes); } return [.. validatableTypes]; diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs index 30de31e208b0..693160ad06ce 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs @@ -15,6 +15,7 @@ public async Task CanValidateIValidatableObject() using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Validation; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -24,7 +25,11 @@ public async Task CanValidateIValidatableObject() var app = builder.Build(); -app.MapPost("/validatable-object", (ComplexValidatableType model) => Results.Ok()); +app.MapPost("/validatable-object", ( + ComplexValidatableType model, + // Demonstrates that parameters that are annotated with [FromService] are not processed + // by the source generator and not emitted as ValidatableTypes in the generated code. + [FromServices] IRangeService rangeService) => Results.Ok(rangeService.GetMinimum())); app.Run(); diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs index 3be6e96fe7a9..b82ad93e507c 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs @@ -16,21 +16,27 @@ public async Task CanValidateParameters() using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Validation; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(); builder.Services.AddValidation(); +builder.Services.AddSingleton(); var app = builder.Build(); app.MapGet("/params", ( + // Skipped from validation because it is resolved as a service by IServiceProviderIsService + TestService testService, [Range(10, 100)] int value1, [Range(10, 100), Display(Name = "Valid identifier")] int value2, [Required] string value3 = "some-value", [CustomValidation(ErrorMessage = "Value must be an even number")] int value4 = 4, - [CustomValidation, Range(10, 100)] int value5 = 10) => "OK"); + [CustomValidation, Range(10, 100)] int value5 = 10, + // Skipped from validation because it is marked as a [FromService] parameter + [FromServices] [Range(10, 100)] int? value6 = 4) => "OK"); app.Run(); @@ -38,6 +44,12 @@ public class CustomValidationAttribute : ValidationAttribute { public override bool IsValid(object? value) => value is int number && number % 2 == 0; } + +public class TestService +{ + [Range(10, 100)] + public int Value { get; set; } = 4; +} """; await Verify(source, out var compilation); await VerifyEndpoint(compilation, "/params", async (endpoint, serviceProvider) => diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs index 2c1052c8f20a..487bb503da95 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs @@ -60,6 +60,11 @@ public GeneratedValidatableTypeInfo( public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) { validatableInfo = null; + if (type == typeof(global::TestService)) + { + validatableInfo = CreateTestService(); + return true; + } return false; } @@ -71,6 +76,20 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn return false; } + private ValidatableTypeInfo CreateTestService() + { + return new GeneratedValidatableTypeInfo( + type: typeof(global::TestService), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::TestService), + propertyType: typeof(int), + name: "Value", + displayName: "Value" + ), + ] + ); + } } diff --git a/src/Http/Routing/src/ValidationEndpointFilterFactory.cs b/src/Http/Routing/src/ValidationEndpointFilterFactory.cs index 0e75ea640fa9..1117cb428115 100644 --- a/src/Http/Routing/src/ValidationEndpointFilterFactory.cs +++ b/src/Http/Routing/src/ValidationEndpointFilterFactory.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -19,6 +21,8 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context return next; } + var serviceProviderIsService = context.ApplicationServices.GetService(); + var parameterCount = parameters.Length; var validatableParameters = new IValidatableInfo[parameterCount]; var parameterDisplayNames = new string[parameterCount]; @@ -26,6 +30,12 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context for (var i = 0; i < parameterCount; i++) { + // Ignore parameters that are resolved from the DI container. + if (IsServiceParameter(parameters[i], serviceProviderIsService)) + { + continue; + } + if (options.TryGetValidatableParameterInfo(parameters[i], out var validatableParameter)) { validatableParameters[i] = validatableParameter; @@ -70,6 +80,13 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context }; } + private static bool IsServiceParameter(ParameterInfo parameterInfo, IServiceProviderIsService? isService) + => HasFromServicesAttribute(parameterInfo) || + (isService?.IsService(parameterInfo.ParameterType) == true); + + private static bool HasFromServicesAttribute(ParameterInfo parameterInfo) + => parameterInfo.CustomAttributes.OfType().Any(); + private static string GetDisplayName(ParameterInfo parameterInfo) { var displayAttribute = parameterInfo.GetCustomAttribute(); From f5a03e76fe7f28d7603d025064ef35c88d6b1d19 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 13 May 2025 07:28:06 -0700 Subject: [PATCH 2/4] Update src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Parsers/ValidationsGenerator.TypesParser.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs index dc8f7463e672..c3fde3f17910 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs @@ -34,10 +34,11 @@ internal ImmutableArray ExtractValidatableTypes(IInvocationOper foreach (var parameter in parameters) { + // Skip attributes that implement the IFromServiceMetadata interface. + // These attributes are used for dependency injection (DI) purposes and do not require validation. if (parameter.GetAttributes().Any(attr => attr.AttributeClass is not null && attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol))) { continue; - } _ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes); } From 14842fc2aac1964ddb81fa3233278e1e8e608be3 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 13 May 2025 14:53:18 +0000 Subject: [PATCH 3/4] Add back brace --- .../Parsers/ValidationsGenerator.TypesParser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs index c3fde3f17910..5719311d4906 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs @@ -39,6 +39,7 @@ internal ImmutableArray ExtractValidatableTypes(IInvocationOper if (parameter.GetAttributes().Any(attr => attr.AttributeClass is not null && attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol))) { continue; + } _ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes); } From 359e54a11b113f918ac441a5cb1dab70f3ffdcf7 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 14 May 2025 14:28:31 -0700 Subject: [PATCH 4/4] Fix up handling for keyed services --- .../Extensions/ITypeSymbolExtensions.cs | 14 ++++++++++++++ .../Parsers/ValidationsGenerator.TypesParser.cs | 7 ++++--- .../ValidationsGenerator.IValidatableObject.cs | 10 +++++++++- .../ValidationsGenerator.Parameters.cs | 3 +++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs index 89392a93849f..0cd85d9df756 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs @@ -124,4 +124,18 @@ internal static bool IsExemptType(this ITypeSymbol type, WellKnownTypes wellKnow return null; } + + /// + /// Checks if the parameter is marked with [FromService] or [FromKeyedService] attributes. + /// + /// The parameter to check. + /// The symbol representing the [FromService] attribute. + /// The symbol representing the [FromKeyedService] attribute. + internal static bool IsServiceParameter(this IParameterSymbol parameter, INamedTypeSymbol fromServiceMetadataSymbol, INamedTypeSymbol fromKeyedServiceAttributeSymbol) + { + return parameter.GetAttributes().Any(attr => + attr.AttributeClass is not null && + (attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) || + SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol))); + } } diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs index 5719311d4906..482a90c334b0 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs @@ -28,15 +28,16 @@ internal ImmutableArray ExtractValidatableTypes(IInvocationOper var fromServiceMetadataSymbol = wellKnownTypes.Get( WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata); + var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get( + WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute); var validatableTypes = new HashSet(ValidatableTypeComparer.Instance); List visitedTypes = []; foreach (var parameter in parameters) { - // Skip attributes that implement the IFromServiceMetadata interface. - // These attributes are used for dependency injection (DI) purposes and do not require validation. - if (parameter.GetAttributes().Any(attr => attr.AttributeClass is not null && attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol))) + // Skip parameters that are injected as services + if (parameter.IsServiceParameter(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol)) { continue; } diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs index 693160ad06ce..c26c09ffbd19 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs @@ -21,6 +21,7 @@ public async Task CanValidateIValidatableObject() var builder = WebApplication.CreateBuilder(); builder.Services.AddSingleton(); +builder.Services.AddKeyedSingleton("serviceKey"); builder.Services.AddValidation(); var app = builder.Build(); @@ -29,7 +30,8 @@ public async Task CanValidateIValidatableObject() ComplexValidatableType model, // Demonstrates that parameters that are annotated with [FromService] are not processed // by the source generator and not emitted as ValidatableTypes in the generated code. - [FromServices] IRangeService rangeService) => Results.Ok(rangeService.GetMinimum())); + [FromServices] IRangeService rangeService, + [FromKeyedServices("serviceKey")] TestService testService) => Results.Ok(rangeService.GetMinimum())); app.Run(); @@ -89,6 +91,12 @@ public class RangeService : IRangeService public int GetMinimum() => 10; public int GetMaximum() => 100; } + +public class TestService +{ + [Range(10, 100)] + public int Value { get; set; } = 4; +} """; await Verify(source, out var compilation); diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs index b82ad93e507c..7494e27efa2f 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs @@ -24,12 +24,15 @@ public async Task CanValidateParameters() builder.Services.AddValidation(); builder.Services.AddSingleton(); +builder.Services.AddKeyedSingleton("serviceKey"); var app = builder.Build(); app.MapGet("/params", ( // Skipped from validation because it is resolved as a service by IServiceProviderIsService TestService testService, + // Skipped from validation because it is marked as a [FromKeyedService] parameter + [FromKeyedServices("serviceKey")] TestService testService2, [Range(10, 100)] int value1, [Range(10, 100), Display(Name = "Valid identifier")] int value2, [Required] string value3 = "some-value",