3

This question is a follow up to this post - How to perform async ModelState validation with FluentValidation in Web API?.

I was wondering if FluentValidation has a way to perform async ModelState validation in .net core web api. I have a FluentValidation Validator class which contains async validation methods such as "MustAsync", which means in my business service class I call the validator manually using "ValidateAsync". I also want to use this same validator class to validate the model coming in from the request. I went through the documents and read that the only way to do this is to manually call the "ValidateAsync()" method since the .net pipeline is synchronous. I would rather not manually have to call this method from within my controller, I would prefer to either register it in the startup (have the framework automatically call the the validator on my model) or decorate my request model with the validator.

Has anyone been able to achieve this?

Thanks!

BryMan
  • 495
  • 2
  • 8
  • 15
  • The linked question provides some sample code for the "old" ASP.NET. However, you can easily adapt that to ASP.NET Core as the general principle remains the same. Did you already try that? – nachtjasmin Mar 25 '19 at 14:32

3 Answers3

4

Based on the linked question, I've adapted the code slightly to be compatible with ASP.NET Core (2.2, in my case). In general, this is using the IAsyncActionFilter interface. You can read about it in the official docs.

public class ModelValidationActionFilter : IAsyncActionFilter
{
    private readonly IValidatorFactory _validatorFactory;
    public ModelValidationActionFilter(IValidatorFactory validatorFactory) => _validatorFactory = validatorFactory;

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var allErrors = new Dictionary<string, object>();

        // Short-circuit if there's nothing to validate
        if (context.ActionArguments.Count == 0)
        {
            await next();
            return;
        }

        foreach (var (key, value) in context.ActionArguments)
        {
            // skip null values
            if (value == null)
                continue;

            var validator = _validatorFactory.GetValidator(value.GetType());

            // skip objects with no validators
            if (validator == null)
                continue;

            // validate
            var result = await validator.ValidateAsync(value);

            // if it's valid, continue
            if (result.IsValid) continue;

            // if there are errors, copy to the response dictonary
            var dict = new Dictionary<string, string>();

            foreach (var e in result.Errors)
                dict[e.PropertyName] = e.ErrorMessage;

            allErrors.Add(key, dict);
        }

        if (allErrors.Any())
        {
            // Do anything you want here, if the validation failed.
            // For example, you can set context.Result to a new BadRequestResult()
            // or implement the Post-Request-Get pattern.
        }
        else
            await next();
    }
}

If you want to apply this filter globally, you can add the filter to the AddMvc call in your Startup class. For example:

 services.AddMvc(options => 
{
    options.Filters.Add<ModelValidationActionFilter>();

    // uncomment the following line, if you want to disable the regular validation
    // options.ModelValidatorProviders.Clear();
});
nachtjasmin
  • 386
  • 4
  • 13
1

I had trouble getting the code in @nachtjasmin's answer to work with newer versions of FluentValidation. Specifically, the trouble is that ValidateAsync now takes an IValidationContext instead of the model being validated, and the context can't be created without knowing the type of the model at compile time.

Eventually I stumbled upon this answer, which points out that the exact type is not important and uses object instead.

So, instead of:

var result = await validator.ValidateAsync(value);

You can use:

var context = new ValidationContext<object>(value);
var result = await validator.ValidateAsync(context);
lrpe
  • 730
  • 1
  • 5
  • 13
0

Based on the answer above by @nachtjasmin, you can add this in two ways,

  1. Using AddMvc

    services.AddControllersWithViews(options =>
    {
        options.Filters.Add<FluentValidationActionFilter>();
    });
    
  2. Using AddControllersWithViews

    services.AddControllersWithViews(options =>
    {
        options.Filters.Add<FluentValidationActionFilter>();
    });
    

If your's is just a Web API and you don't have any Razor pages involved, then you can consider using AddControllersWithViews over AddMvc, as the AddMvc uses the AddControllersWithViews internally and add the services.AddRazorPages() on top of that.

You can see this info here for AddMvc and here for AddControllersWithViews

Sibeesh Venu
  • 18,755
  • 12
  • 103
  • 140