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.
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.
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 emptyPages
must be greater than 0Languages
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"); } }
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"
Note that you can throw an exception if an object is invalid by using the ValidateAndThrow()
method.
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.
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(); } }
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."); } }
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");
FluentValidation provides default error codes for each rule, such as:
NotNullValidator
NotEmptyValidator
EqualValidator
LengthValidator
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);