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

sergiobarriel

Sergio Barriel

Posted on June 10, 2021

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

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);
}
Enter fullscreen mode Exit fullscreen mode

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

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Alt Texthttps://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) { }
}
Enter fullscreen mode Exit fullscreen mode

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>();
        });
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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": ""
}
Enter fullscreen mode Exit fullscreen mode
{
    "Title": [
        "'Title' should not be empty."
    ],
    "Content": [
        "'Content' should not be empty."
    ]
}
Enter fullscreen mode Exit fullscreen mode

Notice: This post was published originally on Medium platform on may 2, 2018

💖 💪 🙅 🚩
sergiobarriel
Sergio Barriel

Posted on June 10, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024

How to Use KitOps with MLflow
beginners How to Use KitOps with MLflow

November 29, 2024