How to automatically validate a model with MVC filter and Fluent Validation package

The most elegant way to validate view models using ASP.NET MVC and .NET Core 2.0

I have had the opportunity to see many MVC projects and in the vast majority of them I find structures where the view models are manually validated in all the methods of all the controllers.

At best, the code relies on verifying ModelState property from Controller class, but in the worst case… better not to comment it.

[Route("")]
[HttpPost]
public async Task<IActionResult> CreatePostAsync([FromBody] CreatePost request)
{
    if (ModelState.IsValid)
    {
        return Ok(await _postService.CreateAsync(request));
    }
    return BadRequest(ModelState.ValidationState);
}

So, it exists a more elegant solution for this… let’s see it

In our example, we will validate a view model called CreatePost before the request reaches the controller PostController, and after that, we will send the request to properly service

Step 1: Install nuget packages

Download FluentValidation.AspNetCore by Jeremy Skinner

Step 2: Create validator

With FluentValidation package, all our validators are classes that inherits from AbstractValidator, where is our view model.

In this step, we’re making sure that the “Title” and “Content” properties from “CreatePost” view model are never empty, like [Required] data annotation.

public class CreatePostValidator : AbstractValidator<CreatePost>
{
    public CreatePostValidator()
    {
        RuleFor(x => x.Title).NotEmpty();
        RuleFor(x => x.Content).NotEmpty();
    }
}

As you can see, in class constructor, you can add rules for each property and combine as you need.

This is a more complete example with conditional rules for a more flexible scenario, where we can allow authentication with a username or email, but always one of the two.

public class LoginValidator : AbstractValidator<Login>
{
    public LoginValidator()
    {
        RuleFor(x => x.UserName)
            .NotEmpty()
            .When(x => !DataTypesHelper.IsEmail(x.Email) && string.IsNullOrEmpty(x.Email))
            .WithMessage("Username or email are required");

        RuleFor(x => x.Email)
            .NotEmpty()
            .When(x => string.IsNullOrEmpty(x.UserName))
            .WithMessage("Username or email are required");

        RuleFor(x => x.Password)
            .NotEmpty()
            .WithMessage("Password is required");
    }
}

If you need, you can read the complete documentation about FluentValidation from Github repository

Step 3: Create MVC filter

As shown on this post about MVC lifecycle by Matthew Jones, Action Filters are invoked before and after controller, but we are interested in validating our model only before invoking the controller, so for this reason we will use OnActionExecuting method.

https://www.asp.net/media/4071077/aspnet-web-api-poster.pdf

At this point, we will check ModelState, which internally will apply the rules that we have defined with FluentValidation

public class ModelStateFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
    public void OnActionExecuted(ActionExecutedContext context) { }
}

Code example is based on this Gist

Step 4: Register services into DI container

When adding the MVC service to the DI container, we must specify the action filters that we want to use, and then, specify in which assembly are the validators of our view models.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
        {
            options.Filters.Add(new ModelStateFilter());
        })
        .AddFluentValidation(options =>
        {
            options.RegisterValidatorsFromAssemblyContaining<Startup>();
        });
}

Step 5: Create controller method

The last step is to create a method in our controller, which meets the requirement of being lightweight.

[Route("")]
[HttpPost]
public async Task<IActionResult> CreatePostAsync([FromBody] CreatePost request)
{
    return Ok(await _postService.CreateAsync(request));
}

If you invoke the method with the wrong view model, you will receive a response like “400 Bad Request” with the errors summary.

POST /post HTTP/1.1
Host: localhost:50555
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: c5f7b803-dfe2-a315-eb8f-671b84cb3175

{
	"Title": "",
	"Content": "",
	"Excerpt": ""
}

And JSON response should be like:

{
    "Title": [
        "'Title' should not be empty."
    ],
    "Content": [
        "'Content' should not be empty."
    ]
}