Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -25,10 +25,22 @@ 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 validatableTypes = new HashSet<ValidatableType>(ValidatableTypeComparer.Instance);
List<ITypeSymbol> 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)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also add a check for [FromKeyedServicesAttribute] (and test of course)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep -- added a test to check for the [FromKeyedServices].

{
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,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;

Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,40 @@ 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>();

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

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