Chargement...

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


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


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


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


TL;DR

  • FluentValidation sépare les règles du modèle pour une validation plus lisible et testable.
  • RuleFor, RuleForEach et SetValidator sont 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