Skip to content

Instantly share code, notes, and snippets.

@ejsmith
Created August 29, 2023 04:51
Show Gist options
  • Save ejsmith/25bf3a9979432e63b4b7d437b03405c3 to your computer and use it in GitHub Desktop.
Save ejsmith/25bf3a9979432e63b4b7d437b03405c3 to your computer and use it in GitHub Desktop.
Auto validation
using System.IO.Pipelines;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
using MiniValidation;
using WebApplication6.Validation;
namespace WebApplication6.Validation
{
public class AutoValidationActionFilter : IAsyncActionFilter
{
private readonly IServiceProvider serviceProvider;
public AutoValidationActionFilter(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.Controller is ControllerBase controllerBase && context.ActionDescriptor is ControllerActionDescriptor actionDescriptor)
{
var parametersToValidate = actionDescriptor.MethodInfo.GetParameters()
.Where(p => p.GetCustomAttributes(typeof(ValidateAttribute), true) != null
&& ShouldValidate(p.ParameterType, serviceProvider as IServiceProviderIsService)).ToArray();
foreach (var parameter in parametersToValidate)
{
if (context.ActionArguments.TryGetValue(parameter.Name, out var subject) && subject != null)
{
var (isValid, errors) = await MiniValidator.TryValidateAsync(subject, serviceProvider, recurse: true);
if (!isValid)
{
foreach (var error in errors)
{
foreach (var errorMessage in error.Value)
{
context.ModelState.AddModelError(error.Key, errorMessage);
}
}
}
}
}
if (!context.ModelState.IsValid)
{
var validationProblem = controllerBase.ProblemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState);
context.Result = new BadRequestObjectResult(validationProblem);
return;
}
}
await next();
}
private static bool ShouldValidate(Type type, IServiceProviderIsService? isService = null) =>
!IsNonValidatedType(type, isService)
&& MiniValidator.RequiresValidation(type);
private static bool IsNonValidatedType(Type type, IServiceProviderIsService? isService) =>
typeof(HttpContext) == type
|| typeof(HttpRequest) == type
|| typeof(HttpResponse) == type
|| typeof(ClaimsPrincipal) == type
|| typeof(CancellationToken) == type
|| typeof(IFormFileCollection) == type
|| typeof(IFormFile) == type
|| typeof(Stream) == type
|| typeof(PipeReader) == type
|| isService?.IsService(type) == true;
}
}
public static class ValidationExtensions
{
public static IServiceCollection AddAutoValidation(this IServiceCollection serviceCollection)
{
// Create a default instance of the `ModelStateInvalidFilter` to access the non static property `Order` in a static context.
var modelStateInvalidFilter = new ModelStateInvalidFilter(new ApiBehaviorOptions { InvalidModelStateResponseFactory = context => new OkResult() }, NullLogger.Instance);
// Make sure we insert the `FluentValidationAutoValidationActionFilter` before the built-in `ModelStateInvalidFilter` to prevent it short-circuiting the request.
serviceCollection.Configure<MvcOptions>(options => options.Filters.Add<AutoValidationActionFilter>(modelStateInvalidFilter.Order - 1));
return serviceCollection;
}
}
[AttributeUsage(AttributeTargets.Parameter)]
public class ValidateAttribute : Attribute
{
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment