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 dd0092f31fd0..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 @@ -25,10 +25,23 @@ 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 fromKeyedServiceAttributeSymbol = wellKnownTypes.Get( + WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute); + var validatableTypes = new HashSet(ValidatableTypeComparer.Instance); List visitedTypes = []; + foreach (var parameter in parameters) { + // Skip parameters that are injected as services + if (parameter.IsServiceParameter(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol)) + { + 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..c26c09ffbd19 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs @@ -15,16 +15,23 @@ 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; var builder = WebApplication.CreateBuilder(); builder.Services.AddSingleton(); +builder.Services.AddKeyedSingleton("serviceKey"); builder.Services.AddValidation(); 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, + [FromKeyedServices("serviceKey")] TestService testService) => Results.Ok(rangeService.GetMinimum())); app.Run(); @@ -84,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 3be6e96fe7a9..7494e27efa2f 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs @@ -16,21 +16,30 @@ 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(); +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", [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 +47,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();