Skip to content

Exempt parameters resolved from DI from validation #61895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,18 @@ internal static bool IsExemptType(this ITypeSymbol type, WellKnownTypes wellKnow

return null;
}

/// <summary>
/// Checks if the parameter is marked with [FromService] or [FromKeyedService] attributes.
/// </summary>
/// <param name="parameter">The parameter to check.</param>
/// <param name="fromServiceMetadataSymbol">The symbol representing the [FromService] attribute.</param>
/// <param name="fromKeyedServiceAttributeSymbol">The symbol representing the [FromKeyedService] attribute.</param>
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)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,23 @@ internal ImmutableArray<ValidatableType> 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<ValidatableType>(ValidatableTypeComparer.Instance);
List<ITypeSymbol> 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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IRangeService, RangeService>();
builder.Services.AddKeyedSingleton<TestService>("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();

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,43 @@ 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<TestService>();
builder.Services.AddKeyedSingleton<TestService>("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();

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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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"
),
]
);
}

}

Expand Down
17 changes: 17 additions & 0 deletions src/Http/Routing/src/ValidationEndpointFilterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -19,13 +21,21 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context
return next;
}

var serviceProviderIsService = context.ApplicationServices.GetService<IServiceProviderIsService>();

var parameterCount = parameters.Length;
var validatableParameters = new IValidatableInfo[parameterCount];
var parameterDisplayNames = new string[parameterCount];
var hasValidatableParameters = false;

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;
Expand Down Expand Up @@ -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<IFromServiceMetadata>().Any();

private static string GetDisplayName(ParameterInfo parameterInfo)
{
var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>();
Expand Down
Loading