La validation des données est un pilier fondamental de tout logiciel robuste. Elle agit comme un garde-fou, prévenant l’introduction d’informations erronées ou mal formatées qui pourraient compromettre l’intégrité et la stabilité du système, sécuriser les traitements et protéger l’application contre les données malveillantes ou erronées. Les méthodes traditionnelles de validation, comme les attributs de validation ou les vérifications manuelles avec des blocs if/else, deviennent rapidement difficiles à maintenir et à faire évoluer quand le code devient complexe.
FluentValidation est une bibliothèque .NET open-source qui permet de définir des règles de validation de manière intuitive et réutilisable. Contrairement aux attributs de validation traditionnels comme [Range]
ou [StringLength]
, FluentValidation offre une approche plus flexible et expressive en utilisant la syntaxe Fluent API. Elle sépare la logique de validation des modèles, ce qui rend le code plus maintenable et testable.
Supposons la classe Book
suivante :
public class Book { public string Title { get; set; } public int Pages { get; set; } public string Language { get; set; } }
Nous voulons vérifier la valeur de chaque propriété avec ces conditions :
Title
ne doit pas être videPages
doit être supérieur à 0Languages
doit être égale à “french” ou “english” FluentValidation fournit la classe AbstractValidator<T>
(où T
est la classe à valider, ici c’est Book
). Pour créer un validateur, il suffit de créer une classe qui hérite de AbstractValidator
comme ceci :
public class BookValidator : AbstractValidator<Book> { public BookValidator() { RuleFor(book => book.Title) .NotEmpty() .MinimumLength(5); RuleFor(book => book.Pages) .GreaterThan(0); } }
Le constructeur de la classe BookValidator
définit les règles de validation en utilisant la méthode RuleFor()
. Cette méthode reçoit en argument une expression lambda qui retourne la propriété de la classe Book
à vérifier. Ainsi, il suffit d’ajouter par la suite les règles que nous voulons appliquer à cette propriété (dans cet exemple, Title
ne doit pas être vide et contenir au minimum 5 caractères).
Les méthodes NotEmpty()
et MinimumLength()
sont fournies par FluentValidation. Il existe une multitude de règles fournies par la librairie prêtes à être utilisées (GreaterThanOrEqualTo
, InclusiveBetween
, NotEqual
, Matches
, EmailAddress
…).
Il est aussi possible de créer des règles personnalisées en utilisant la méthode Must()
. Dans notre exemple, Must()
est implémenté comme ceci pour la propriété Language
:
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"); } }
Une fois le validateur défini, il suffit de créer une nouvelle instance de cette classe et utiliser la méthode Validate()
comme ceci :
Book book = new() { Title = "Harry Potter", Pages = 200, Language = "french" }; BookValidator bookValidator = new(); ValidationResult validationResult = bookValidator.Validate(book); Console.WriteLine(validationResult.IsValid); //log "True"
Cette méthode retourne un objet de type ValidationResult
. Celui-ci expose une propriété IsValid
qui indique si l’objet book
respecte les critères définis.
Si l’objet book
ne respecte pas toutes les règles, ValidationResult
définira IsValid
sur false
et fournira une liste d’erreurs détaillant les règles non respectées :
Book book = new() { Title = "test", Pages = -3, Language = "spanish" }; BookValidator bookValidator = new(); ValidationResult validationResult = bookValidator.Validate(book); Console.WriteLine(validationResult.IsValid); //log "False"
À noter qu’il est possible de lever une exception si un objet n’est pas valide en utilisant la méthode ValidateAndThrow()
.
Supposons une classe Library
contenant une liste d’objets Book
définie précédemment :
public class Library { public string Name { get; set; } public string Address { get; set; } public List<Book> Books { get; set; } }
En plus de vérifier la validité des propriétés Name
et Address
, il faut aussi vérifier que chaque élément de la liste Books
respecte les règles définies précédemment. FluentValidation met à disposition la méthode SetValidator()
qui prend en argument un validateur héritant de la classe AbstractValidator<T>
comme ceci :
public class LibraryValidator : AbstractValidator<Library> { public LibraryValidator() { RuleFor(lib => lib.Name).NotEmpty(); RuleFor(lib => lib.Address).NotEmpty(); RuleForEach(lib => lib.Books).SetValidator(new BookValidator()); } }
NB : RuleForEach()
permet d’appliquer les règles de validation sur tous les éléments d’une liste.
Nous allons ajouter dans notre classe Book
une propriété IsEbook
de type booléen permettant de savoir si un livre est en version physique ou numérique :
public class Book { public string Title { get; set; } public int Pages { get; set; } public string Language { get; set; } public bool IsEbook { get; set; } }
Nous voulons maintenant ajouter une règle de taille maximale de 10 caractères pour le titre d’un livre uniquement si IsEbook
est true
. Pour cela, FluentValidation fournit la méthode When()
qui prend en paramètre une expression lambda avec la condition à respecter :
public class BookValidator : AbstractValidator<Book> { public BookValidator() { RuleFor(book => book.Title) .NotEmpty() .MaximumLength(10) .When(book => book.IsEbook == true); } }
À noter que la condition s’applique à toutes les règles définies avant When()
. Dans cet exemple, si IsEbook
est false
, FluentValidation ne vérifiera pas les conditions NotEmpty()
et MaximumLength(10)
. Pour vérifier que le titre n’est pas vide pour tous les cas, il suffit de le placer après When()
:
public class BookValidator : AbstractValidator<Book> { public BookValidator() { RuleFor(book => book.Title) .MaximumLength(10) .When(book => book.IsEbook == true) .NotEmpty(); } }
Il est possible de personnaliser le message d’erreur pour chaque règle en utilisant la méthode WithMessage()
:
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."); } }
Vous pouvez également donner un code d’erreur, que ValidationResult
pourra ensuite utiliser pour gérer les cas d’erreur lors de l’exécution :
RuleFor(book => book.Language) .NotEmpty() .Must(lang => lang is "french" or "english") .WithErrorCode("InvalidLanguage");
FluentValidation fournit par défaut des codes d’erreur pour chaque règle définis comme :
NotNullValidator
NotEmptyValidator
EqualValidator
LengthValidator
Comme vu précédemment, il faut créer une nouvelle instance de la classe du validateur pour pouvoir l’utiliser par la suite. FluentValidation permet aussi d’utiliser l’injection de dépendance en enregistrant le service dans Program.cs
avec IValidator<T>
(T
étant le modèle à valider) :
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; } }
Il faut enregistrer chaque validateur dans les services de l’application. Une alternative est d’utiliser le package Nuget FluentValidation.DependencyInjectionExtensions
qui permet d’enregistrer automatiquement tous les validateurs définis dans l’Assembly comme ceci :
builder.Services.AddValidatorsFromAssemblies([Assembly.GetExecutingAssembly()], ServiceLifetime.Transient);