Utilisation de FluentValidation en C#
Apprenez FluentValidation en C# pour créer des règles de validation claires et maintenables.
FluentValidation est une bibliothèque populaire en C# qui permet de définir des règles de validation à l’aide d’une API fluide. Elle permet de créer des règles réutilisables et testables, indépendantes des classes de modèle. Cet article propose un guide pratique – de l’installation aux fonctionnalités avancées, y compris l’intégration avec ASP.NET Core et les tests unitaires.
Installation
Pour le package de base et l’intégration optionnelle avec ASP.NET Core :
dotnet add package FluentValidation
dotnet add package FluentValidation.AspNetCore # Pour les projets web
Pour les projets de bureau (WPF/WinForms) ou les services, seul FluentValidation est nécessaire.
Utilisation de base : Un validateur simple
Une classe de validateur hérite de AbstractValidator<T>, et les règles sont définies à l’aide de RuleFor.
using FluentValidation;
public class UserDto
{
public string? Name { get; set; }
public string? Email { get; set; }
public int Age { get; set; }
}
public class UserDtoValidator : AbstractValidator<UserDto>
{
public UserDtoValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Le nom ne peut pas être vide")
.Length(2, 50).WithMessage("Le nom doit contenir entre 2 et 50 caractères");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("L’adresse e-mail est obligatoire")
.EmailAddress().WithMessage("Veuillez entrer une adresse e-mail valide");
RuleFor(x => x.Age)
.InclusiveBetween(18, 120).WithMessage("L’âge doit être compris entre 18 et 120 ans");
}
}
Pour valider : var result = new UserDtoValidator().Validate(model);
Les erreurs peuvent être consultées via result.Errors.
Messages, Gravité et Mode en cascade
- Utilisez
WithMessage()pour des messages d’erreur personnalisés. - Utilisez
WithSeverity(Severity.Error|Warning|Info)pour définir une priorité. - Utilisez
Cascade(CascadeMode.Stop)pour arrêter la validation après la première erreur.
RuleFor(x => x.Name)
.Cascade(CascadeMode.Stop)
.NotEmpty().WithMessage("Le nom est obligatoire").WithSeverity(Severity.Error)
.MinimumLength(2).WithMessage("Le nom doit comporter au moins 2 caractères");
Règles conditionnelles : When / Unless
Utilisé pour exécuter une règle uniquement lorsqu’une condition spécifique est remplie.
RuleFor(x => x.Email)
.NotEmpty().EmailAddress();
RuleFor(x => x.Age)
.GreaterThan(0);
RuleFor(x => x)
.Must(x => x.Email!.EndsWith("@company.com"))
.When(x => x.Age >= 18)
.WithMessage("Les utilisateurs de 18 ans et plus doivent utiliser une adresse e-mail professionnelle");
Objets associés et collections
Utilisez SetValidator pour valider des modèles imbriqués et RuleForEach pour valider les éléments d’une collection.
public class AddressDto { public string? City { get; set; } public string? Zip { get; set; } }
public class AddressValidator : AbstractValidator<AddressDto>
{
public AddressValidator()
{
RuleFor(x => x.City).NotEmpty();
RuleFor(x => x.Zip).NotEmpty().Length(5, 10);
}
}
public class CustomerDto
{
public string? Name { get; set; }
public AddressDto? Address { get; set; }
public List<string> Phones { get; set; } = new();
}
public class CustomerValidator : AbstractValidator<CustomerDto>
{
public CustomerValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Address)
.NotNull().WithMessage("L’adresse est requise")
.SetValidator(new AddressValidator());
RuleForEach(x => x.Phones)
.NotEmpty().Matches(@"^\+?\d{7,15}$")
.WithMessage("Numéro de téléphone invalide");
}
}
Règles personnalisées et asynchrones
Utilisez Must/MustAsync pour des validations complexes ou dépendant d’un service externe.
RuleFor(x => x.Email)
.MustAsync(async (email, ct) =>
{
// Exemple : vérification d’unicité (pseudo-repository)
bool exists = await _userRepository.ExistsByEmailAsync(email, ct);
return !exists;
})
.WithMessage("L’adresse e-mail est déjà utilisée");
Validation basée sur le scénario avec RuleSet
Vous pouvez regrouper les règles pour différents scénarios (par ex. Create, Update).
public class ProductDto { public string? Name { get; set; } public decimal Price { get; set; } }
public class ProductValidator : AbstractValidator<ProductDto>
{
public ProductValidator()
{
RuleSet("Create", () =>
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Price).GreaterThan(0);
});
RuleSet("Update", () =>
{
RuleFor(x => x.Price).GreaterThanOrEqualTo(0);
});
}
}
// Utilisation :
// validator.Validate(model, options: o => o.IncludeRuleSets("Create"));
Personnalisation et Localisation des Messages d’Erreur
- Utilisez
WithName("Nom du champ")pour afficher un nom de champ convivial à l’écran. - Gérez le multilinguisme avec des fichiers de ressources (.resx).
- Le LanguageManager peut être personnalisé pour des messages globaux.
RuleFor(x => x.Email)
.WithName("E-mail")
.NotEmpty().WithMessage("{PropertyName} ne peut pas être vide")
.EmailAddress().WithMessage("{PropertyName} n’est pas valide");
Intégration avec ASP.NET Core
Enregistrez les services pour activer la validation automatique des modèles et l’intégration avec ModelState.
// Program.cs (Minimal API / ASP.NET Core 7+)
using FluentValidation;
using FluentValidation.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddFluentValidationAutoValidation(); // Fonctionne automatiquement avec le model binding MVC
builder.Services.AddValidatorsFromAssemblyContaining<UserDtoValidator>(); // Analyse tous les validateurs
var app = builder.Build();
app.MapControllers();
app.Run();
Dans un contrôleur, aucun appel manuel n’est nécessaire : après le model binding, FluentValidation s’exécute automatiquement et écrit les erreurs dans ModelState.
Sujets Avancés : Dépendances, Injection et Règles Réutilisables
- Injectez des services ou des dépôts dans le constructeur du validateur pour les utiliser dans MustAsync.
- Partagez les règles répétées avec
Include:
public class NameRules : AbstractValidator<UserDto>
{
public NameRules()
=> RuleFor(x => x.Name).NotEmpty().MaximumLength(50);
}
public class UserDtoValidator2 : AbstractValidator<UserDto>
{
public UserDtoValidator2()
{
Include(new NameRules());
RuleFor(x => x.Email).EmailAddress();
}
}
Testabilité : FluentValidation.TestHelper
Utilisez les extensions d’aide pour tester rapidement vos validateurs.
dotnet add package FluentValidation.TestHelper
using FluentValidation.TestHelper;
using Xunit;
public class UserDtoValidatorTests
{
[Fact]
public void Nom_Ne_Doit_Pas_Etre_Vide()
{
var v = new UserDtoValidator();
var result = v.TestValidate(new UserDto { Name = "" });
result.ShouldHaveValidationErrorFor(x => x.Name);
}
[Fact]
public void Email_Doît_Etre_Valide()
{
var v = new UserDtoValidator();
var result = v.TestValidate(new UserDto { Email = "x" });
result.ShouldHaveValidationErrorFor(x => x.Email);
}
}
Exemple Réel : Formulaire d’Inscription + Adresse & Téléphones
L’exemple suivant montre une validation complète : inscription utilisateur, adresse imbriquée et liste de numéros de téléphone.
public class RegisterRequest
{
public string? FullName { get; set; }
public string? Email { get; set; }
public AddressDto? Address { get; set; }
public List<string> Phones { get; set; } = new();
}
public class RegisterValidator : AbstractValidator<RegisterRequest>
{
public RegisterValidator(IEmailBlacklistService blacklist)
{
RuleFor(x => x.FullName)
.NotEmpty().WithMessage("Le nom complet est requis")
.MaximumLength(80);
RuleFor(x => x.Email)
.NotEmpty().EmailAddress()
.MustAsync(async (mail, ct) => !await blacklist.IsBlacklistedAsync(mail, ct))
.WithMessage("L’e-mail n’est pas accepté");
RuleFor(x => x.Address)
.NotNull().SetValidator(new AddressValidator());
RuleForEach(x => x.Phones)
.NotEmpty()
.Matches(@"^\+?\d{7,15}$").WithMessage("Format de téléphone invalide");
// Conditionnel : Si le pays de l’adresse est 'FR', le code postal doit comporter 5 chiffres
When(x => x.Address?.Country == "FR", () =>
{
RuleFor(x => x.Address!.Zip)
.Matches(@"^\d{5}$").WithMessage("En France, le code postal doit comporter 5 chiffres");
});
}
}
Performance et Bonnes Pratiques
- Écrivez des validateurs sans état ; injectez uniquement les dépendances nécessaires dans le constructeur.
- Réduisez les appels MustAsync intensifs en I/O grâce à la mise en cache dans les environnements à fort trafic.
- Utilisez
RuleSetpour les validations basées sur des scénarios dans les grands modèles. - Gardez les messages d’erreur courts et conviviaux ; personnalisez les noms des champs avec
WithName. - Complétez la validation côté serveur par une validation côté client lorsque nécessaire.
TL;DR
- FluentValidation sépare les règles du modèle pour une validation plus lisible et testable.
RuleFor,RuleForEachetSetValidatorsont les éléments essentiels.- Utilisez When/Unless pour les règles conditionnelles et RuleSet pour les validations par scénario.
- Must/MustAsync est idéal pour les vérifications personnalisées ou via un service externe.
- Dans ASP.NET Core, activez l’intégration automatique avec
AddFluentValidationAutoValidation(). - Sécurisez vos validateurs avec des tests unitaires à l’aide de TestHelper.
Articles connexes
Classes, Objets, Propriétés et Méthodes en C#
Découvrez comment les classes, objets, propriétés et méthodes en C# constituent les fondements de la programmation orientée objet.
Principes de l’Injection de Dépendances en C#
Apprenez les principes de l’Injection de Dépendances en C#, la gestion des dépendances et le couplage faible avec exemples.