Fluent validation

Published by Oussama BOUAZZA
Category : .Net
03/07/2025

Data validation is a fundamental pillar of any robust software.
It acts as a safeguard, preventing the introduction of incorrect or poorly formatted information that could compromise the system’s integrity and stability, secure processing, and protect the application from malicious or faulty data.
Traditional validation methods—such as validation attributes or manual checks using if/else blocks—quickly become difficult to maintain and evolve as the codebase grows more complex.

 

 

What is FluentValidation?

 

FluentValidation is an open-source .NET library that allows you to define validation rules in an intuitive and reusable way.
Unlike traditional validation attributes like [Range] or [StringLength], FluentValidation offers a more flexible and expressive approach using Fluent API syntax.
It separates validation logic from models, making the codebase easier to maintain and test.

 

 

Creating Your First Validation Rules

 

Let’s assume the following Book class:

 

public class Book
{
    public string Title { get; set; }
    public int Pages { get; set; }
    public string Language { get; set; }
}

 

We want to validate each property with the following conditions:

  • Title must not be empty
  • Pages must be greater than 0
  • Languages must be either “french” or “english”

FluentValidation provides the AbstractValidator<T> class (where T is the class to be validated—in this case, Book).
To create a validator, simply define a class that inherits from AbstractValidator, like this:

 

public class BookValidator : AbstractValidator<Book>
{
    public BookValidator()
    {
        RuleFor(book => book.Title)
            .NotEmpty()
            .MinimumLength(5);
            
        RuleFor(book => book.Pages)
            .GreaterThan(0);
    }
}

 

The constructor of the BookValidator class defines the validation rules using the RuleFor() method.
This method takes a lambda expression that returns the property of the Book class to validate.
You can then chain the rules you want to apply to that property—for example, ensuring that Title is not empty and has at least 5 characters.

The methods NotEmpty() and MinimumLength() are provided by FluentValidation.
The library includes a wide range of built-in rules ready to use, such as GreaterThanOrEqualTo, InclusiveBetween, NotEqual, Matches, EmailAddress, and more.

You can also create custom rules using the Must() method.
In our example, Must() is implemented like this for the Language property:

 

public class BookValidator : AbstractValidator<Book>
{
    public BookValidator()
    {
        RuleFor(book => book.Title)
            .NotEmpty()
            .MinimumLength(5);
        RuleFor(book => book.Pages)
            .GreaterThan(0);
        
        RuleFor(book => book.Language)
            .NotEmpty()
            .Must(lang => lang is "french" or "english");
    }
}

 

 

Using the Validator at Runtime

 

Once the validator is defined, simply create a new instance of the class and use the Validate() method like this:

 

Book book = new()
{
    Title = "Harry Potter",
    Pages = 200,
    Language = "french"
};

BookValidator bookValidator = new(); 
ValidationResult validationResult = bookValidator.Validate(book); 
Console.WriteLine(validationResult.IsValid); //log "True"

 

This method returns an object of type ValidationResult. It exposes an IsValid property that indicates whether the book object meets the defined criteria.

If the book object fails to meet one or more rules, ValidationResult will set IsValid to false and provide a list of errors detailing which rules were violated:

 

Book book = new()
{
    Title = "test",
    Pages = -3,
    Language = "spanish"
};

BookValidator bookValidator = new();
ValidationResult validationResult = bookValidator.Validate(book);
Console.WriteLine(validationResult.IsValid); //log "False"

 

Fluent results

 

Note that you can throw an exception if an object is invalid by using the ValidateAndThrow() method.

 

 

Reusing Validators in Complex Objects

 

Let’s assume a Library class that contains a list of Book objects defined earlier:

 

public class Library
{
    public string Name { get; set; }
    public string Address { get; set; }
    public List<Book> Books { get; set; }
}

 

In addition to validating the Name and Address properties, we also need to ensure that each item in the Books list follows the rules we previously defined.

FluentValidation provides the SetValidator() method, which accepts a validator that inherits from the AbstractValidator<T> class, like this:

 

public class LibraryValidator : AbstractValidator<Library>
{
    public LibraryValidator()
    {
        RuleFor(lib => lib.Name).NotEmpty();
        RuleFor(lib => lib.Address).NotEmpty();
         
        RuleForEach(lib => lib.Books).SetValidator(new BookValidator());
    }
}

 

Note: RuleForEach() allows you to apply validation rules to every item in a list.

 

 

Adding Conditional Validation Rules

 

We’re going to add a new property to our Book class called IsEbook, a boolean indicating whether the book is a physical or digital version:

 

public class Book
{
    public string Title { get; set; }
    public int Pages { get; set; }
    public string Language { get; set; }
    public bool IsEbook { get; set; }
}

 

We now want to add a rule that limits the book title to a maximum of 10 characters, but only if IsEbook is true.
To achieve this, FluentValidation provides the When() method, which takes a lambda expression representing the condition to meet:

 

public class BookValidator : AbstractValidator<Book>
{
    public BookValidator()
    {
        RuleFor(book => book.Title)
            .NotEmpty()
            .MaximumLength(10)
            .When(book => book.IsEbook == true);
    }
}

 

Note that the condition applies to all rules defined before When().
In this example, if IsEbook is false, FluentValidation will not check the NotEmpty() and MaximumLength(10) rules.
To ensure the title is never empty, regardless of the condition, simply place the NotEmpty() rule after the When() block:

 

public class BookValidator : AbstractValidator<Book>
{
    public BookValidator()
    {
        RuleFor(book => book.Title)
            .MaximumLength(10)
            .When(book => book.IsEbook == true)
            .NotEmpty();
    }
}

 

 

Customizing Error Messages

 

You can customize the error message for each rule using the WithMessage() method:

 

public class BookValidator : AbstractValidator<Book>
{
    public BookValidator()
    {
        RuleFor(book => book.Title)
            .NotEmpty().WithMessage(" Book title must not be empty")
            .MinimumLength(5).WithMessage(" Book title must be at least 5 characters long");
        RuleFor(book => book.Pages)
            .GreaterThan(0)
            .WithMessage("Le nombre de pages doit être supérieur à 0");
        
        RuleFor(book => book.Language)
            .NotEmpty()
            .Must(lang => lang is "french" or "english")
            .WithMessage(book => $"'{book.Language}' is not a valid language. Only 'french' and 'english' are allowed.");
    }
}

 

Fluent results 2

 

You can also assign an error code using the WithErrorCode() method.
This code can later be used by ValidationResult to handle specific error cases at runtime:

 

RuleFor(book => book.Language)
            .NotEmpty()
            .Must(lang => lang is "french" or "english")
            .WithErrorCode("InvalidLanguage");

 

Fluent results 3

 

FluentValidation provides default error codes for each rule, such as:

  • NotNullValidator
  • NotEmptyValidator
  • EqualValidator
  • LengthValidator

 

 

Registering a Validator with Dependency Injection

 

As previously shown, you need to create a new instance of the validator class to use it.
However, FluentValidation also supports dependency injection by registering the service in Program.cs using IValidator<T> (where T is the model to validate):

 

builder.Services.AddTransient<IValidator<Book>, BookValidator>();
builder.Services.AddTransient<IValidator<Library>, LibraryValidator>();

 

public class MyClass
{
    private IValidator<Book> _bookValidator;
    
    public MyClass(IValidator<Book> bookValidator)
    {
        _bookValidator = bookValidator;
    }
    
    
    public bool ValidateBook(Book book)
    {
        return _bookValidator.Validate(book).IsValid;
    }
}

 

Each validator must be registered in the application’s service collection.
An alternative is to use the NuGet package FluentValidation.DependencyInjectionExtensions, which allows you to automatically register all validators defined in the assembly like this:

 

builder.Services.AddValidatorsFromAssemblies([Assembly.GetExecutingAssembly()], ServiceLifetime.Transient);

 

Source: Official FluentValidation documentation