Introduction :

Dans la première partie nous avons parlé des attributs d’annotations de données qui nous permettent d’appliquer certaines règles de validations assez simples sur notre modèle. Comme dit dans la conclusion de cette précédente partie, aucun des attributs d’annotations déjà présents dans le Framework ne permet de vérifier que l’heure de début est inférieure à l’heure de fin d’une session sans écrire un peu plus de code. Dans cette article nous verrons trois solutions qu’on peut utiliser pour mettre en place des règles de validations personnalisées :

  • utilisation de l’attribut CustomValidation.
  • création d’un attribut d’annotations de données en dérivant de la classe ValidationAttribute.
  • implémentation de l’interface IValidatableObject.

Utilisation de l’attribut CustomValidation :

Le constructeur de la classe CustomValidation reçoit 2 paramètres avec en premier le type de la classe où se trouve la méthode à exécuter dont le nom est passé en second paramètre.

La méthode à exécuter doit être statique et doit renvoyer une instance de type ValidationResultQuant aux paramètres en entrée nous avons 2 options :

  • créer une méthode avec un seul paramètre de type object. Ce paramètre peut être fortement typé avec le type de la classe ou de la propriété sur laquelle s’applique l’attribut.
  • créer une méthode avec deux paramètres. Le premier paramètre de type object peut être fortement typé. Le deuxième paramètre quant à lui doit obligatoirement être du type ValidationContext.

Notre cas exemple doit vérifier que l’heure de début est inférieure à l’heure de fin de la session et que la durée est d’une heure alors mettre l’attribut au niveau de classe est plus simple vu que la logique de validation impliquera 2 propriétés de notre modèle.

La classe contenant la méthode de validation est définie dans le code ci-dessous :

public static class SessionValidations
{
   public static ValidationResult CheckSessionHours(Session session)
   {
   if((session.EndHour - session.StartHour) != 1)
   {
   return new ValidationResult("End hour must be greater than start hour and the session duration must be equal to 1 hour", new string[] { "StartHour", "EndHour" });
   } 
   return ValidationResult.Success;
   }
}

Le code ci-dessous montre l’application de cette règle de validation à travers l’utilisation de l’attribut CustomValidation :

[CustomValidation(typeof(SessionBusinessValidations), "CheckSessionHours")] 
public class Session 
{  
   // … 
}

Nous n’avons pas créé une méthode statique recevant 2 paramètres. Cela est dû au fait que pour notre cas nous n’avons tout simplement pas besoin de l’instance de type ValidationContext. Le paramètre value typé en Session répond largement à notre besoin en nous permettant d’accéder aux deux propriétés impliquées dans la validation et ainsi vérifier que les horaires et la durée sont correctes.

IServiceProvider La classe ValidationContext décrit le contexte dans lequel se déroule la validation et propose des membres intéressants dont :

  • la méthode GetService (un contrat de l’interface IServiceProvier) permet de tirer parti de l’injection de dépendance. Il peut arriver que nous ayons besoin de solliciter la base de données pour pouvoir effectuer certaines validations. Comme exemple, nous devons normalement, avant d’ajouter une nouvelle session, vérifier que la salle dans laquelle doit avoir lieu la session est libre aux horaires choisies. Si nous utilisons le pattern repository, nous pouvons utiliser la méthode GetService pour accéder à une instance de ce repository sans pour autant avoir à l’instancier de façon explicite et cela pour limiter le couplage.
  • la propriété Items qui est un dictionnaire permettant de stocker certaines informations qui pourront transiter d’une validation à l’autre.

Création d’une classe dérivée de ValidationAttribute :

Tous les attributs d’annotations de données dérivent de la classe ValidationAttribute. Notre attribut d’annotations personnalisé devra lui aussi dériver de cette classe. La classe ValidationAttribute est définie comme abstraite mais ne contient aucun membre abstrait donc en gros aucun contrat à implémenter mis à part que certains membres sont virtuels. Pour implémenter notre logique de validation nous devons redéfinir l’une des surcharges, toutes virtuelles, de la méthode IsValid. Redéfinir dans notre classe dérivée les deux surcharges de IsValid ne sert absolument à rien. Voyons voir pourquoi la redéfinition d’une seule des surcharges de IsValid suffit ?

IsValid

Dans le processus de validation de notre modèle dans ASP.Net Web API, pour tout attribut d’annotations un appel à la méthode GetValidationResult (héritée de la classe de base ValidationAttribute) est effectué. Voici l’implémentation de cette méthode en question après décompilation :

ValidationAttribute Dans l’image ci-dessus nous voyons l’appel à la surcharge IsValid (object value, ValidationContext context). Si nous avons redéfini cette surcharge dans notre classe dérivée alors pas de problème. Dans le cas contraire la méthode exécutée est celle de la classe de base dont l’implémentation est dans l’image ci-dessous et c’est là qu’est fait l’appel à la méthode IsValid (object value) :

object value

L’implémentation de la méthode IsValid (object value) est montrée dans l’image ci-dessous et elle aussi fait appel à la surcharge IsValid (object value, ValidationContext context) :

IsValid

On se retrouve avec 2 surcharges et chacune d’elle fait appel à l’autre. Pour éviter un StackOverflowException, si une des surcharges est exécutée alors le champ _isCallingOverload est mis à true durant son exécution. Lors de l’appel à l’autre surcharge s’il se trouve que _isCallingOverload a la valeur true alors une exception NotImplementedException est lancée. Pour éviter cette exception, obligatoirement l’une des deux surcharges de la méthode IsValid doit être ré-implémentée dans la classe dérivée.

Si nous ré-implémentons la méthode IsValid (object value, ValidationContext context) alors le déroulement de la validation s’enchainera comme suit :

  1. Exécution de la méthode GetValidationResults héritée de la classe de base.
  2. Appel à la méthode IsValid (object value, ValidationContext context) de notre de classe dérivée.

Dans le cas de la ré-implémentation de la méthode IsValid (object value), nous aurons l’enchaînement suivant :

  1. Exécution de la méthode GetValidationResults héritée de la classe de base.
  2. Appel à la méthode IsValid (object value, ValidationContext context) de notre classe de base.
  3. Appel à la méthode IsValid (object value) de notre classe dérivée.

Si au sein de notre attribut d’annotation personnalisé nous redéfinissons ces 2 méthodes alors on peut dire, au vu des 2 enchaînements précédents, que la méthode IsValid (obejct value) ne sera jamais exécutée d’où le fait que seule l’une des 2 surcharges de IsValid doit être redéfinie notre classe dérivée. Le choix de l’une des 2 surcharges à redéfinir dans notre classe dérivée dépendra vraiment de l’utilité que l’on portera à l’instance de type ValidationConext (l’intérêt de cette classe a été expliquée dans la première section).

Pour notre cas exemple nous n’avons pas besoin de l’instance de ValidationContext ainsi voilà à quoi ressemble notre attribut d’annotations personnalisé :

[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class CheckSessionHoursAttribute : ValidationAttribute
{
   public override bool IsValid(object value)
   {
   Session session = value as Session;
   if (session == null)
   {
   throw new InvalidOperationException("This attribute must be used on Session class");
   } 
   return session.EndHour - session.StartHour == 1;
   }
}

Notre attribut ne peut être utilisé que pour décorer une classe. La méthode IsValid lance une exception si le paramètre value ne peut être converti en une instance de type Session. Pour l’utiliser c’est simple :

[CheckSessionHoursAttribute(ErrorMessage="End hour must be greater than start hour and the session duration must be equal to 1 hour")]
public class Session
{
   // ...
}

Implémenter l’interface IValidatableObject :

Jusque là nous avons utilisé les attributs d’annotations placés correctement sur notre modèle pour que nos règles de validation soient prises en compte. Nous pouvons passer par autre chose que l’utilisation des attributs en implémentant l’interface IValidatableObject. Cet interface doit être implémenté par notre modèle et propose la méthode Validate comme le montre l’image ci-dessous. Cette méthode reçoit en paramètre une instance de ValidationContext et retourne une collection d’instances de type ValidationResultreprésentant la liste des erreurs rencontrées.

namespace

La méthode Validate est exécutée uniquement si toutes les règles de validations appliquées à notre modèle via les attributs d’annotations de données sont valides.

Dans le cas de notre classe Session, nous aurons le code ci-dessous :

public class Session: IValidatableObject
{
   // ...
   /// <summary>
   /// Determines whether the specified object is valid.
   /// </summary>
   /// <param name="validationContext">The validation context.</param>
   /// <returns>
   /// A collection that holds failed-validation information.
   /// </returns>
   /// <exception cref="System.NotImplementedException"></exception>
   public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
   {
   var results = new List<ValidationResult>(); 
   if(this.StartHour >= this.EndHour)
   yield return new ValidationResult("The end hour must be greater than the start hour.", new string[] { "StartHour", "EndHour" }); 
   if (this.EndHour - this.StartHour != 1)
   yield return new ValidationResult("The duration must be equal to 1 hour.", new string[] { "StartHour", "EndHour" });
   }
}

Conclusion :

Dans cet article nous avons utilisé trois façons permettant de mettre en place une validation personnalisée. Avec les deux premières solutions nous avons manipulé principalement des attributs soit en tirant parti de l’attribut CustomValidation fourni par le Framework .Net ou en codant un nouvel attribut dérivant de la classe abstraite ValidationAttribute. La troisième solution quant à elle est totalement différente des deux premières vu que dans celle-ci nous utilisons l’interface IValidatableObject. La question qu’on doit se poser est : comment choisir entre ces 3 solutions ? On peut se baser sur les points suivants :

  • si la logique de validation doit être réutilisable (indépendamment du modèle sur lequel s’applique la validation) alors il faut penser à créer un attribut dérivant de classe de base ValidationAttribute comme cela est effectué dans la deuxième solution.
  • si la logique de validation existe déjà donc susceptible d’être fortement liée au modèle sur lequel on doit l’appliquer et qu’on veut quand même tirer parti des avantages apportés par les attributs d’annotations de données alors l’attribut CustomValidation fera l’affaire.
  • enfin si on n’aime pas avoir des attributs par-ci et par-là et polluer notre modèle ou si la logique de validation personnalisée doit être appelée uniquement si toutes les propriétés décorées par des attributs d’annotations sont valides alors choisir la solution proposant l’utilisation de l’interface IValidatableObject. N’oublions pas aussi que cet interface nécessite l’implémentation de la méthode Validate qui renvoie une collection d’instances de type ValidationResult ce qui nous permet de retourner le résultat de plusieurs validations et cela n’est pas possible avec les attributs d’annotations de données.

Dans la première et la deuxième partie nous avons modifié notre modèle représenté par la classe Session pour y appliquer des règles de validations. Dans la troisième et dernière partie nous verrons une façon fluente de mettre en place nos règles de validations et cela sans altérer notre modèle avec la librairie Fluent Validation créée par le développeur Jeremy Skinner.

Ne ratez plus aucune actualité avec la newsletter mensuelle de SoftFluent

Newsletter SoftFluent