| File: ValidationEndpointFilterFactory.cs | Web Access |
| Project: src\aspnetcore\src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj (Microsoft.AspNetCore.Routing) |
#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using System.Net.Mime; using System.Reflection; using System.Security.Claims; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Validation; namespace Microsoft.AspNetCore.Http.Validation; internal static class ValidationEndpointFilterFactory { // A small struct to hold the validatable parameter details to avoid allocating arrays for parameters that don't need validation private readonly record struct ValidatableParameterEntry(int Index, IValidatableParameterInfo Parameter); public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context, EndpointFilterDelegate next) { var parameters = context.MethodInfo.GetParameters(); var options = context.ApplicationServices.GetService<IOptions<ValidationOptions>>()?.Value; if (options is null || options.Resolvers.Count == 0) { return next; } var serviceProviderIsService = context.ApplicationServices.GetService<IServiceProviderIsService>(); // Use a list to only store validatable parameters instead of arrays for all parameters List<ValidatableParameterEntry>? validatableParameters = null; for (var i = 0; i < parameters.Length; 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 ??= []; validatableParameters.Add(new ValidatableParameterEntry( i, validatableParameter)); } } if (validatableParameters is null || validatableParameters.Count == 0) { return next; } return async (context) => { ValidateContext? validateContext = null; foreach (var entry in validatableParameters) { if (entry.Index >= context.Arguments.Count) { break; } var argument = context.Arguments[entry.Index]; validateContext ??= new ValidateContext { ValidationOptions = options, ServiceProvider = context.HttpContext.RequestServices, }; await entry.Parameter.ValidateAsync(argument, validateContext, context.HttpContext.RequestAborted); } if (validateContext is { ValidationErrors.Count: > 0 }) { context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; var validationErrors = validateContext.ValidationErrors.ToDictionary( keySelector: kvp => kvp.Key, elementSelector: kvp => kvp.Value.Select(e => e.ErrorMessage).ToArray()); var problemDetails = new HttpValidationProblemDetails(validationErrors) { Status = StatusCodes.Status400BadRequest }; var problemDetailsService = context.HttpContext.RequestServices.GetService<IProblemDetailsService>(); if (problemDetailsService is not null) { if (await problemDetailsService.TryWriteAsync(new() { HttpContext = context.HttpContext, ProblemDetails = problemDetails })) { // We need to prevent further execution, because the actual // ProblemDetails response has already been written by ProblemDetailsService. return EmptyHttpResult.Instance; } } // Fallback to the default implementation. context.HttpContext.Response.ContentType = MediaTypeNames.Application.ProblemJson; return problemDetails; } return await next(context); }; } private static bool IsServiceParameter(ParameterInfo parameterInfo, IServiceProviderIsService? isService) { var attributes = parameterInfo.GetCustomAttributes(); foreach (var attribute in attributes) { if (attribute is IFromRouteMetadata or IFromQueryMetadata or IFromHeaderMetadata or IFromBodyMetadata or IFromFormMetadata) { return false; } if (attribute is IFromServiceMetadata or FromKeyedServicesAttribute) { return true; } } var parameterType = parameterInfo.ParameterType; return parameterType == typeof(HttpContext) || parameterType == typeof(HttpRequest) || parameterType == typeof(HttpResponse) || parameterType == typeof(ClaimsPrincipal) || parameterType == typeof(CancellationToken) || isService?.IsService(parameterType) == true; } }