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();