Search code examples
c#.net-corefluentvalidationminimal-apis.net-7.0

Making use of Filters in .NET 7


I have been using Minimal APIs since it was released in .NET 6. For our validation I've been using the manual approach as follows:

app.MapPost("api/user", async ([FromService] IValidator<UserDto> validator, [FromBody] UserDto user) => 
{
   var validationResult = await validator.ValidateAsync(user);

   if (!validationResult.IsValid)
   {
      return Results.BadRequest(string.Join("/n", validationResult.Errors));
   }
  
  ...
})

With the new release of .NET 7 including Filters. I have gone ahead and implemented some of the features. I've created the custom validation filter as follows:

public class ValidationFilter<T> : IEndpointFilter where T : class
{
 private readonly IValidator<T> _validator;

 public ValidationFilter(IValidator<T> validator)
 {
    _validator = validator;
 }

 public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
 {
    var obj = context.Arguments.FirstOrDefault(x => x?.GetType() == typeof(T)) as T;

    if (obj is null)
    {
        return Results.BadRequest();
    }
    
    var validationResult = await _validator.ValidateAsync(obj);

    if (!validationResult.IsValid)
    {
        return Results.BadRequest(string.Join("/n", validationResult.Errors));
    }

    return await next(context);
  }
}

I can now use the above by calling AddEndPointFilter<T>() so something like:

app.MapPost("api/user", (..) => { ... }).AddEndPointFilter<ValidationFilter>();

The above works great. However, I have some RuleSet() in my FluentValidation which I include in a PUT request. So my question is, how can I pass the RuleSets to my ValidationFilter?


Solution

  • One way is to leverage the ability to provide metadata for endpoint. Something along this lines:

    public class RuleSetMetadata<T>
    {
        public RuleSetMetadata(string ruleSet)
        {
            RuleSet = ruleSet;
        }
    
        public string RuleSet { get; set; }
    }
    

    Setup:

    app.MapPost("api/user", (Example e) =>  e)
        .AddEndpointFilter<ValidationFilter<Example>>()
        .WithMetadata(new RuleSetMetadata<Example>("Test"))
    

    And changes to the implementation:

    public class ValidationFilter<T> : IEndpointFilter where T : class
    {
        // ...
    
        public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
        {
            string? ruleSet = null;
            if (context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<RuleSetMetadata<T>>() is {} meta)
            {
                ruleSet = meta.RuleSet;
            }
    
            var validationResult = ruleSet is null
                ? await _validator.ValidateAsync(obj)
                : await _validator.ValidateAsync(obj, options => options.IncludeRuleSets(ruleSet));
    
            // ...
        }
    }
    

    Another way is to look into AddEntpointFilterFactory and implement some parameter handling via attributes (can be used in conjunction with var grp = app.MapGroup(""); grp.AddEndpointFilterFactory(...)).