Conception d'une interface fluide pour la validation de données en C#

Dans le développement d'applications web, la validation des formulaires est une étape incontournable. Bien que la validation côté client améliore l'expérience utilisateur, elle reste insuffisante en matière de sécurité. N'importe quel outil de requête HTTP permet de contourner les scripts du navigateur. Il est donc impératif de procéder à une seconde vérification côté serveur avant tout traietment métier ou persistance en base de données.

Traditionnellement, la validation en C# repose sur une accumulation de structures conditionnelles if. Prenons l'exemple d'une inscription utilisateur nécessitant un numéro de mobile et un code de vérification :

// Approche classique
if (model == null) return "Paramètres invalides";
if (string.IsNullOrEmpty(model.Phone)) return "Téléphone requis";
if (string.IsNullOrEmpty(model.Code)) return "Code requis";

Cette approche devient rapidement verbeuse et répétitive à mesure que le nombre de champs augmente. Pour pallier ce manque d'élégance, l'adoption d'un style de programmation fluide (Fluent Interface), similaire à ce que propose LINQ ou jQuery, permet de rendre le code plus lisible et concis.

L'idée consiste à encapsuler l'objet à valider dans un conteneur d'état et d'utiliser des méthodes d'extension pour enchaîner les tests. Voici une structure de base pour gérer cet état :

public class ValidationFlow<T>
{
    public T Target { get; }
    public string FailureMessage { get; private set; }
    public bool HasFailed => !string.IsNullOrEmpty(FailureMessage);

    public ValidationFlow(T target)
    {
        Target = target;
    }

    public void RegisterError(string message)
    {
        if (!HasFailed) FailureMessage = message;
    }
}

Pour permettre le chaînage, nous définissons une méthode d'extension. Une subtilité importante ici est la gestion de la sécurité : si une étape de la chaîne échoue (par exemple, si l'objet est null), les étapes suivantes ne doivent pas tenter d'accéder aux propriétés de l'objet pour éviter une exception de référence nule (NullReferenceException).

public static class FluentValidatorExtensions
{
    public static ValidationFlow<T> Ensure<T>(
        this ValidationFlow<T> flow, 
        Predicate<T> condition, 
        string errorMessage)
    {
        // Si une erreur a déjà été détectée, on ignore les validations suivantes
        if (flow.HasFailed) return flow;

        try
        {
            if (!condition(flow.Target))
            {
                flow.RegisterError(errorMessage);
            }
        }
        catch
        {
            // Capture les erreurs si le prédicat tente d'accéder à un membre d'un objet null
            flow.RegisterError(errorMessage);
        }

        return flow;
    }
}

Grâce à cette structure, la mise en œuvre de la logique de validation devient beaucoup plus fluide et visuelle :

var validation = new ValidationFlow<UserRegistration>(request)
    .Ensure(r => r != null, "Requête vide")
    .Ensure(r => !string.IsNullOrWhiteSpace(r.Phone), "Le numéro de téléphone est obligatoire")
    .Ensure(r => r.Phone.Length == 10, "Format de téléphone invalide")
    .Ensure(r => !string.IsNullOrWhiteSpace(r.Code), "Le code de vérification est requis");

if (validation.HasFailed)
{
    return BadRequest(validation.FailureMessage);
}

Cette approche présente plusieurs avantages techniques :

  • Lisibilité accrue : Chaque règle de gestion est isolée sur une ligne, facilitant la compréhension immédiate des contraintes.
  • Réutilisabilité : Il est aisé d'ajouter des méthodes d'extension spécifiques (ex: IsEmail(), IsMatch()) pour enrichir la bibliothèque de validation.
  • Maintenance simplifiée : La suppression ou l'ajout d'une règle ne nécessite pas de modifier l'imbrication des blocs conditionnels.

Cependant, il faut noter que cette implémentation simple continue de parcourir la chaîne de méthodes même après une erreur, bien que le traitement logique à l'intérieur de chaque méthode soit court-circuité. Pour des scénarios extrêmement critiques en termes de performance, l'utilisation de if classiques reste l'option la plus directe, mais pour la grande majorité des applications métier, l'élégance et la maintenabilité du style fluide prévalent.

Étiquettes: dotnet CSharp fluent-interface validation design-patterns

Publié le 25 juillet à 22h23