Introduction
Ce billet est la troisième et dernière partie de la suite d’articles sur le thème ASP.Net Web API et validations. Tout au long des parties 1et 2 nous nous sommes attelés à modifier notre modèle pour y appliquer des règles de validation en :
- décorant les propriétés ou notre classe avec des attributs d’annotations de données
- implémentant l’interface IValidatableObject.
Jusque là il fallait que nous ayons accès à notre modèle pour pouvoir le modifier. Dans certains cas il se peut qu’on ne veuille pas polluer notre modèle et surtout pouvoir centraliser les règles de validation à un seul endroit dans une classe séparée par exemple pour ainsi éviter de devoir naviguer à travers les propriétés de notre classe pour connaitre les règles que le modèle doit respecter. Dans ce présent billet nous allons voir comment la librairie Fluent Validation répond à ces problématiques.
Installation de la librairie
Fluent Validation est une librairie développée par Jon Skinner. Elle est disponible sur Nuget et nous pouvons l’installer via le gestionnaire de packages Nuget ou utiliser la commande suivante dans la console de gestion des packages :
PM> Install-Package FluentValidation
Avec cette librairie nous pouvons définir des règles de validation mais d’autres librairies dépendant de Fluent Validation sont disponibles pour nous faciliter son intégration dans les différentes technologies. Pour ASP.Net Web API, nous utiliserons la librairie FluentValidation. WebApi développée par le même auteur qu’on peut aussi installer via la commande suivante :
PM> Install-Package FluentValidation. WebApi
Centralisation des règles de validation
Pour appliquer des règles de validation à notre modèle Session nous avons besoin que d’un seul attribut ValidatorAttribute défini dans l’espace de noms FluentValidation. Attributes. Cet attribut comporte un constructeur recevant un paramètre de type System. Typereprésentant le type de la classe qui sera le conteneur de toutes nos règles de validation.
Le code ci-dessous montre l’application de cet attribut sur notre modèle :
[Validator(typeof(SessionValidator))]
public class Session
{
// Les propriétés ont été retirées pour plus de clarté
}
SessionValidator est la classe qui contiendra toutes nos règles de validation pour notre cas exemple. Avant de montrer son implémentation nous allons voir dans la section qui va suivre comment créer des règles de validation avec Fluent Validation.
Création des règles de validation
La classe de validation SessionValidator doit obligatoirement dériver de la classe de base générique AbstractValidator<T> où T est le type de notre modèle.
public class SessionValidator : AbstractValidator<Session>
{
}
Avec Fluent Validation, avant l’application des règles de validation nous devons spécifier sur quelle propriété on les veut en utilisant les méthodes RuleFor et RuleForEach héritées de la classe de base AbstractValidator<T>. À la différence de RuleFor, la méthode RuleForEach s’applique sur les propriétés représentant des collections. Ces deux méthodes reçoivent chacune une expression lambda spécifiant le nom de la propriété sur laquelle s’appliquera la ou les règles comme dans le code ci-dessous :
this.RuleFor(p => p.Title);
this.RuleForEach(p => p.Speakers);
Une fois qu’on sait sur quelle propriété les règles doivent s’appliquer, nous pouvons utiliser l’une des règles de validation intégrées à la librairie. Ces règles sont toutes des méthodes d’extensions définies dans la classe DefaultValidatorExtensions :
- NotNull : pas de valeur null autorisée sur la propriété concernée.
- NotEmpty : la propriété de type String ne doit pas être null, vide ou contenir que des espaces blancs.
- Length : vérifie que la propriété de type String doit contenir un nombre exact de caractères ou que ce nombre doit être compris dans un intervalle donné.
- les méthodes d’extensions NotEqual, Equal, LessThan, LessThanOrEqualTo, GreaterThan et GreaterThanOrEqualTo permettent de vérifier que la valeur de la propriété concernée doit respectivement être différente, égale, inférieure strictement, inférieure ou égale, supérieure strictement et supérieure ou égale à une autre valeur donnée. La valeur utilisée pour la comparaison peut être celle d’une autre propriété.
- ExclusiveBetween et InclusiveBetween permettent de vérifier que la valeur de la propriété est contenue dans un intervalle donné en y excluant les bornes pour la première méthode d’extension et les incluant pour la deuxième.
- Matches reçoit en paramètre une expression régulière. Elle permet de vérifier que la valeur de la propriété est bien dans le format attendu. Pour ce qui est de la validation des emails et des numéros de cartes de crédit pas besoin de passer par cette méthode d’extension, il est plus simple d’utiliser les méthodes EmailAddress et CreditCard.
- si aucune des méthodes d’extensions citées dans les précédents points ne vous satisfait alors Must permet de spécifier une expression lambda qui doit renvoyer true si la validation est OK et false dans le cas contraire. MustAsync est identique à Must à la différence qu’elle est asynchrone comme son nom l’indique.
L’utilisation d’une de ces méthodes d’extensions est très simple :
this.RuleFor(p => p.Title).NotNull();
Une propriété pouvant nécessiter plusieurs règles de validation, nous avons la possibilité de chainer les méthodes d’extensions :
this.RuleFor(p => p.Title)
.NotEmpty()
.Length(10, 100);
En appliquant tout cela à notre classe SessionValidator nous aurons le code ci-dessous :
public class SessionValidator : AbstractValidator<Session>
{
public SessionValidator()
{
// Le titre ne doit pas null, ni vide, ni contenir que des espaces blancs.
// Le nombre de caractères doit être compris entre 10 et 100.
this.RuleFor(p => p.Title)
.NotEmpty()
.Length(10, 100);
// Le jour de la session doit être compris entre 1 et 3.
this.RuleFor(p => p.Day)
.InclusiveBetween(1, 3);
// Le niveau est requis.
this.RuleFor(p => p.Level)
.NotEmpty();
// La description est requise et le nombre de caractères doit être compris entre 100 et 500.
this.RuleFor(p => p.Description)
.NotEmpty()
.Length(100, 500);
// L'heure de début doit être comprise entre 8 et 17h et doit être inférieure strictement à l'heure de fin de la session.
// La durée de la session doit être égale à 1 heure
this.RuleFor(p => p.StartHour)
.InclusiveBetween(8, 17)
.LessThan(p => p.EndHour)
.Must((session, startHour) => startHour - session.EndHour == 1)
.WithMessage("The duration must be equal to 1 hour.");
// L'heure de fin doit être comprise entre 9 et 18h et doit être supérieure strictement à l'heure de début de la session.
// La durée de la session doit être égale à 1 heure.
this.RuleFor(p => p.EndHour)
.InclusiveBetween(9, 18)
.GreaterThan(p => p.StartHour)
.Must((session, endhour) => endhour - session.StartHour == 1)
.WithMessage("The duration must be equal to 1 hour.");
// Le tableau Speakers est requis et le nombre de speakers doit être compris de 1 à 3.
this.RuleFor(p => p.Speakers)
.NotNull()
.Must(p => p.Length >= 1 && p.Length <= 3)
.WithMessage("Speakers are required. The number of speaker must be at least 1 and not exceeed 3.");
// Chaque élément du tableau Speaker doit être différent de null, pas vide et ne pas contenir que des espaces vides.
this.RuleForEach(p => p.Speakers)
.NotEmpty();
}
}
Le code ci-dessus tient compte de toutes les règles de validation que nous avons établies dans les précédentes parties pour notre modèle Session et cela en utilisant uniquement les logiques de validation intégrées à Fluent Validation. Aucune validation personnalisée n’a été créée comme ça a été le cas dans la deuxième partie lorsqu’on voulait vérifier la cohérence des heures de début et de fin d’une session. Fluent Validation permet quand même de mettre en place des règles de validation personnalisées si celles déjà fournies ne répondent pas à nos besoins. Pour mettre en place une validation personnalisée, je vous invite à consulter la documentation plus précisément ici.
Intégration avec ASP.Net Web API
En l’état actuel, ASP.Net Web API ne tiendra pas compte de nos règles de validation écrites avec la librairie Fluent Validation. Pour que cela soit le cas il faut, dans la méthode statique Register de la classe WebApiConfig de notre service, faire appel à la méthode statique Configure de la classe FluentValidationModelValidatorProvider définie dans l’espace de noms FluentValidation. WebApi comme le montre le code ci-dessous :
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
FluentValidation.WebApi.FluentValidationModelValidatorProvider.Configure();
}
}
Nous pouvons effectuer les différents tests que nous avons faits dans les précédentes parties et nous aurons les mêmes messages d’erreurs si les règles de validation ne sont pas respectées. Les implémentations de nos actions définies dans le contrôleur SessionsController restent inchangées.
La classe ValidatorFactoryBase
Fluent Validation permet de libérer notre modèle de tous les attributs d’annotations et interfaces utilisés dans les parties 1 et 2. La libraire va un peu plus loin en nous évitant l’utilisation de l’attribut ValidatorAttribute. Le fait de passer par cette méthode nous permet de pouvoir définir des classes de validation pour des modèles dont nous n’avons pas accès au code source ou que nous voulions tout simplement que ces classes restent purement des classes POCO.
La librairie permet de créer une classe de usinage ou factory qui s’occupera d’instancier la classe de validation correspondant à notre modèle. Pour créer la factory nous devons dériver de la classe ValidatorFactoryBase et nous devons implémenter la méthode abstraite CreateInstance. La méthode CreateInstance reçoit en paramètre le type du modèle dont on veut l’instance de la classe de validation. Pour la logique d’instanciation de la classe de validation je préfère passer par de l’injection de dépendances.
public class NinjectValidatorFactory : ValidatorFactoryBase
{
public override IValidator CreateInstance(Type validatorType)
{
return GlobalConfiguration.Configuration.DependencyResolver.GetService(validatorType) as IValidator;
}
}
Dans le code ci-dessus nous déléguons l’instanciation de notre classe de validation au résolveur de dépendances de ASP.Net Web API GlobalConfiguration. Configuration. DependencyResolver. Ce résolveur est par défaut une instance de la classe System. Web. Http. Dependencies. EmptyResolver qui ne fait pas grand chose. Nous utiliserons l’injecteur de dépendances Ninject. Vu que cet article ne traite pas de l‘l’injection de dépendances je vous invite à voir les bases de Ninject sur le site dédié à celui-ci.
Nous utiliserons la librairie WebApiContrib. IoC. Ninject qui nous propose une implémentation d’un résolveur se basant sur Ninject et qui fonctionne parfaitement avec ASP.Net Web API. La libraire est disponible sur Nuget :
PM> Install-Package WebApiContrib. IoC. Ninject
Pour dire à Ninject comment instancier nos classes de validation nous passerons par un module qui contiendra le mappage entre notre modèle et sa classe de validation. Notre module FluentValidationModule est présenté dans le code ci-dessous :
public class FluentValidationModule : NinjectModule
{
public override void Load()
{
// Toutes les classes de validation dérivent de la classe abstraite AbstractValidator<T> qui, elle à son tour, implémente les interfaces IValidator<T> et IValidator
// Le mappage entre IValidator<Session> et SessionValidator tient bien la route
// Nous utilisons une seule instance de la classe de validation pour toutes les requêtes.
this.Bind<IValidator<Session>>()
.To<SessionValidator>()
.InSingletonScope();
}
}
Pour faire fonctionner tout cela nous avons besoin de faire quelques petites modifications dans la méthode Register de notre classe WebApiConfig. Les commentaires expliquent l’utilité de chaque ligne :
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// Nous instancions le kernel standard en lui passant une instance du module Ninject créé précédemment.
var kernel = new StandardKernel(new FluentValidationModule());
// Nous assignons une nouvelle instance de la classe NinjectResolver, le résolveur fourni par la librairie WebApiContrib.IoC.Ninject
GlobalConfiguration.Configuration.DependencyResolver = new WebApiContrib.IoC.Ninject.NinjectResolver(kernel);
// Nous utilisons maintenant la surcharge de la méthode Configure ci-dessous pour permettre à Fluent Validation de prendre en compte le nouveau factory que nous avons créé
FluentValidation.WebApi.FluentValidationModelValidatorProvider.Configure(provider => provider.ValidatorFactory = new NinjectValidatorFactory());
}
}
Après toutes ces modifications, nous pouvons retirer l’attribut ValidatorAttribute que nous avons placé sur la classe Session et les règles de validation continueront de fonctionner.
Conclusion
Dans cette troisième et dernière partie nous avons expliqué comment utiliser la librairie Fluent Validation pour appliquer de façon assez simple des règles de validation sur notre modèle. Nous avons vu comment cette librairie a permis d’externaliser les règles de validation dans une classe dédiée au lieu de modifier le code de notre modèle comme ça a été le cas avec les attributs d’annotations de données et l’implémentation de l’interface IValidatableObject. La seule modification qui a été faite sur notre modèle a été de décorer celui-ci avec l’attribut ValidatorAttribute qui permet à Fluent Validation de faire la correspondance entre la classe de validation et le modèle. Cette librairie permet aussi d’éviter l’utilisation de cet attribut sur des classes auxquelles nous n’avons pas accès grâce à la classe ValidatorFactoryBase. Une fonctionnalité dont je n’ai pas parlé dans ce billet est que Fluent Validation fournit des méthodes d’extension ShouldHaveChildValidator, ShouldHaveValidationErrorFor et ShouldNotHaveValidationErrorFor permettant de mettre en place des tests unitaires vérifiant la présence de certaines règles de validation sur nos classes de validation comme il est expliqué dans la documentation ici.
Références pour les 3 parties :
MSDN : http://www.msdn.com
Fluent Validation : http://fluentvalidation.codeplex.com/
Blog de Jon Skinner : http://www.jeremyskinner.co.uk/