Fluent Validation in Dotnet Core


fluent validation in asp.net core

Fluent validation

Fluent validation is a third party library for validating your models in .NET . It is totally free.

๐Ÿ“ข ๐Ÿ“ Last Updated: 21-August-2024

Why fluent validation?

If you already have used data annotation for validation, then you must be aware of validation in .NET. So you might be thinking why do we need fluent validation.

Fluent validation helps you to separate validation logic from your models. That makes you code clean. If you have complex validation logic, then you want to define it separately rather than making your model unreadable.

If you have small scale project and does not have complex validation logic, then using data annotation makes more sense.

You can use the hybrid validation, it is totally up to you and what suits you best. None of them is good or bad. It depends on your requirements.

๐Ÿ’ปSource Code

๐Ÿ‘‰Branch : Fluent-Validation

Create a project or use existing one

We will create a .net core project with template .Net Core Web Api. If you have existing project (for practicing) , then it is good. Donโ€™t waste your precious time on creating a new one.

fluent_validation_project_type

Folder structure

folder structure

Model

// Models/Person.cs
public class Person
{
 public int Id { get; set; }
 public string ? Name { get; set; }
 public string? Email { get; set; }
 public int Age { get; set; }
}

Easiest way (Data annotation)

Data annotation is the easiest way to validate your models..Nothing is wrong with it. It is fast and well suited for small projects. You must be familiar it with. Below is the example of it.

// Models/Person.cs
public class Person
{
 public int Id { get; set; }
 [Required]
 [MaxLength(50)]
 public string ? Name { get; set; }
 [Required]
 [MaxLength(50)]
 public string? Email { get; set; }
 public int Age { get; set; }
}

Fluent Validation

Fluent validation is a third party library which is free. So you need to install it first.

๐Ÿ‘‰ install-package FluentValidation.DependencyInjectionExtensions

๐Ÿ‘‰ Remove all the data annotation from Person.cs (If you are using)

// Models/Person.cs

public class Person
{
 public int Id { get; set; }
 public string ? Name { get; set; }
 public string? Email { get; set; }
 public int Age { get; set; }
}

Validator

We will create a separate class PersonValidator inside Validators directory. There we will define all the validation logic.

๐Ÿ‘‰ Validators/PersonValidator.cs

//Validators/PersonValidator.cs

using FluentValidation;
using net9demo.Models;

namespace net9demo.Validators;

public class PersonValidator: AbstractValidator<Person>
{
 public PersonValidator()
 {
 RuleFor(person=>person.Name).NotNull().MaximumLength(50);
 RuleFor(person=>person.Email).NotNull().MaximumLength(50);
 }
}

These rules are self explanatory

๐Ÿ‘‰First & Second rule, Name and Email can not be null (but they can be empty string), empty and their maximum length should be 50 characters.

Program.cs

You need some configuration in Program.cs to make validation work.

builder.Services.AddScoped<IValidator<Person\>, PersonValidator>();

Adding validation errors to ModelState

Create a folder named โ€œExtensionsโ€, inside this folder create a class named โ€œExtensionsโ€.

public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState)
{
 foreach (var error in result.Errors)
 {
 modelState.AddModelError(error.PropertyName, error.ErrorMessage);
 }
}

AddToModelState method will add all errors to the ModelState.

Person Controller

To use the validator, we have to inject the IValidator<Person> into PersonController and check the validation manually. If validation fails, we will return UnprocessableEntity(),which returns 422 Unprocessable Entity status code along with validation errors.

using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using net9demo.Models;
using net9demo.Repositories;
namespace net9demo.Controllers;

[Route("api/people")]
[ApiController]
public class PersonController : ControllerBase
{
 private readonly IValidator<Person> _validator;
 private readonly IPersonRepository personRepository;
 private readonly ILogger<PersonController> logger;

    public PersonController(IValidator<Person> validator, IPersonRepository personRepository, ILogger<PersonController> logger)
    {
        _validator = validator;
        this.personRepository = personRepository;
        this.logger = logger;
    }


    [HttpPost]
    public async Task<IActionResult> AddPerson(Person person )
    {
        // checking validation
        var personValidator = await _validator.ValidateAsync(person);
        if (!personValidator.IsValid)
        {
            personValidator.AddToModelState(ModelState);
            return UnprocessableEntity(ModelState);
        }

        try
        {
            return CreatedAtAction(nameof(AddPerson),person);
        }
        catch (Exception ex)
        {
            logger.LogError(ex.Message);
            return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
        }
    }

}

Test with swagger ui

req

request

res

response

Email validator

RuleFor(person=>person.Email).NotNull().MaximumLength(50).EmailAddress();

Regular Expression and custom message

RuleFor(person => person.Password)
.NotEmpty()
.NotNull()
.MaximumLength(20)
.Matches("^(?=.*\\d).*$")
.WithMessage("Password must contain numeric value");

Final โ€˜PersonValidatorโ€™ class

using FluentValidation;
using net9demo.Models;

namespace net9demo.Validators;

public class PersonValidator: AbstractValidator<Person>
{
    public PersonValidator()
    {
      RuleFor(person=>person.Name).NotEmpty().NotNull().MaximumLength(50);
      RuleFor(person=>person.Email).NotEmpty().NotNull().MaximumLength(50).EmailAddress();
      RuleFor(person => person.Age).NotNull();
      RuleFor(person => person.Password).NotEmpty().NotNull().MaximumLength(20).Matches("^(?=.*\\d).*$").WithMessage("Password must contain numeric value");
    }
}

Adding Category Validator

Letโ€™s create a new validator class named CategoryValidator inside the Validators folder.

public class CategoryValidator:AbstractValidator<Category>
{
 public CategoryValidator()
 {
 RuleFor(c => c.Name).NotEmpty().NotNull().MinimumLength(2).MaximumLength(20);
 }
}

Register it in the Program.cs

builder.Services.AddScoped<IValidator<Category\>, CategoryValidator\>();

Register all the validator at once (test them also)

If we have n validators, we have to register then n-times. But we can register them all at one. Just add the following line in Program.cs :

// either comment these two lines or remove them.
//builder.Services.AddScoped<IValidator<Person>, PersonValidator>();
//builder.Services.AddScoped<IValidator<Category>, CategoryValidator>();

builder.Services.AddValidatorsFromAssemblyContaining<PersonValidator>();

builder.Services.AddValidatorsFromAssemblyContaining<PersonValidator>()

It will register all the validators in the same assembly in which PersonValidator is defined.

๐Ÿ‘‰For more detail please visit official docs of fluent validation.


Original post by Ravindra Devrani on December 15, 2023.

Canonical link

Exported from Medium on February 19, 2025.