Introduction :
Le Framework .Net, grâce à la techno ASP.Net Web API, permet de mettre en place facilement un service REST. Après la conception du service REST, il faut que nous l’exposions au monde extérieur afin que d’autres développeurs puissent le consommer à travers des applications tierces. Ces développeurs peuvent interagir avec notre service en récupérant des données et aussi nous en transmettre. Toute application qui se respecte doit effectuer la vérification des données transmises par les utilisateurs (qu’ils soient développeurs avertis ou non) avant de lancer tout traitement et cela pour se prémunir de l’insertion des données incohérentes en base de données et des plantages de l’application.
Pour coder notre service REST avec ASP.Net Web API nous manipulons en général 2 couches : le contrôleur et le modèle. Les données manipulées par le service étant localisées au niveau du modèle, c’est sur cette couche que nous allons appliquer les règles de validation. Dans cette première partie de l’article ASP.Net Web API et validations, nous verrons comment mettre en place cette validation avec les attributs d’annotations de données.
Notre service REST exemple :
Tout au long de l’article nous utiliserons un service Web API simple proposant un seul contrôleur SessionsController avec les 5 actions suivantes :
- 2 actions Get : une sans paramètre renvoyant la liste des sessions. L’autre renvoyant la session dont l’identifiant est spécifié en paramètre.
- Post permettant de créer une nouvelle session.
- Put pour la mise à jour d’une session existante.
- Delete pour la suppression d’une session correspondant à l’identifiant passé en paramètre.
public class SessionsController : ApiController
{
public async Task<IHttpActionResult> Get()
{
// Traitement à effectuer
}
public async Task<IHttpActionResult> Get(int id)
{
// Traitement à effectuer
}
public async Task<IHttpActionResult> Post(Session session)
{
// Traitement à effectuer
}
public async Task<IHttpActionResult> Put(Session session)
{
// Traitement à effectuer
}
public async Task<IHttpActionResult> Delete(int id)
{
// Traitement à effectuer
}
}
La classe Session représentant notre modèle est définie dans le code ci-dessous. Il s’agira de mettre en place des règles de validations sur ce modèle afin que toute session reçue par notre API soit valide avant tout traitement.
public enum Level
{
Discovery = 1,
Intermediate,
Confirmed,
Expert
}
public class Session
{
public int Id { get; set; }
public string Title { get; set; }
public Level Level { get; set; }
public string Description { get; set; }
public string[] Speakers { get; set; }
public int Day { get; set; }
public int StartHour { get; set; }
public int EndHour { get; set; }
}
Avertissement : l’action Post n’a aucunement besoin de la propriété Id de la classe Session et donc pour éviter l’over-posting il vaut mieux passer par un DTO (Data Transfert Object). Pour faire simple je n’ai pas créé de DTO pour la suite.
La propriété ModelState :
Tous les contrôleurs Web API dérivent de la classe de base ApiController. Cette dernière définit une propriété ModelState de type ModelStateDictionary qui propose à son tour une propriété booléenne IsValid qui nous permet de déterminer l’état du modèle. En prenant comme exemple l’action Post nous aurons le code suivant :
public async Task<IHttpActionResult> Post(Session session)
{
try
{
if (!ModelState.IsValid) return BadRequest(ModelState);
// Traitement à effectuer
return Ok(session);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
Si le modèle n’est pas valide aucun traitement n’est effectué et nous arrêtons l’exécution et renvoyons à l’utilisateur les erreurs rencontrées. Lors de nos tests nous verrons comment ces erreurs sont présentées à l’utilisateur.
Dans la suite nous allons nous focaliser uniquement sur l’action Post de notre contrôleur vu que la manière de valider le modèle restera le même le bloc if à répéter lorsque nous aurons besoin de coder les autres actions où il sera nécessaire de faire une validation.
Maintenant que nous pouvons déterminer l’état de notre modèle voyons voir comment mettre en place les règles de validations grâce aux attributs d’annotations de données.
Les attributs d’annotations de données :
Les attributs d’annotations de données existent depuis la version 4.0 de .Net et sont tous localisés dans l’espace de noms System. ComponentModel. DataAnnotations (la bibliothèque de classes portant le même nom). Ils dérivent tous de la classe de base ValidationAttribute et sont utilisés pour décorer les différentes propriétés sur lesquelles doivent s’appliquer les règles de validation (ex : Required, MinLength etc). Comme le montre de diagramme de classes, le Framework .Net nous fourni pas mal d’attributs pour nous faciliter la tâche :
Dans le code ci-dessous nous avons appliqué certains de ces attributs d’annotations sur notre modèle :
public class Session
{
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Title { get; set; }
[Required]
[EnumDataType(typeof(Level))]
public Level Level { get; set; }
[Required]
[StringLength(500, MinimumLength = 100)]
public string Description { get; set; }
[Required]
[MinLength(1), MaxLength(3)]
public string[] Speakers { get; set; }
[Required]
[Range(1, 3)]
public int Day { get; set; }
[Required]
[Range(8, 17)]
public int StartHour { get; set; }
[Required]
[Range(9, 18)]
public int EndHour { get; set; }
}
Dans notre modèle nous avons défini toutes nos propriétés comme requises grâce à l’attribut Required obligeant l’utilisateur à les renseigner sauf pour l’Id. L’action Post étant utilisée pour la création d’une nouvelle session alors nous nous occuperons de la génération de l’Id après que toutes les règles soient validées. Mis à part l’attribut Required, nous avons :
- spécifié que la description d’une session doit faire au minimum 100 caractères et ne pas dépasser les 500 grâce à l’attribut StringLength. Il peut sembler bizarre d’avoir les attributs StringLength et Required sur la même propriété. On peut s’attendre à ce que StringLength signale une erreur de validation si notre propriété est vide. Ce n’est pas le cas et on est obligé de faire appel à l’attribut Required pour éviter que l’utilisateur oublie de fournir la description et permettre à StringLength de vérifier le nombre de caractères saisies par l’utilisateur pour la description de la session.
- utilisé les attributs MinLength et MaxLength sur la propriété Speakers. Cela nous permet de spécifier qu’une session doit avoir au moins un speaker et au maximum trois. Ces deux attributs ne peuvent pas s’appliquer pas sur des collection de type List, ICollectionetc. mais uniquement sur un tableau comme c’est le cas pour la propriété Speakers. Il faut aussi noter qu’on peut remplacer l’attribut StringLength, cité dans le précédent point, par l’utilisation de MinLength ou MaxLength vu qu’avant tout un String peut être traité en que tableau de caractères.
- appliqué l’attribut EnumDataType sur la propriété Level. EnumDataType dérive directement de la classe de base DataType (qui elle dérive de ValidationAttribute). Toutes les classes dérivant de DataType permettent de valider le type ou le format d’une propriété. Dans notre cas cela nous assurera que la donnée reçue par notre API REST sera bien convertible en l’une des valeurs de l’énumérateur Level : Discovery, Intermediate, Confirmed et Expert.
- utilisé l’attribut Range pour définir une plage d’horaires sur l’heure de début qui doit être choisie entre 7h et 17h, pareil pour l’heure de fin qui doit être comprise entre 9 et 18. Toute session doit se dérouler dans l’une des journées 1, 2 ou 3.
Test des règles de validations :
Pour tester notre service, j’utilise l’extension Chrome Postman Rest Client pour composer mes requêtes mais rien ne vous empêche d’utiliser Fiddler ;-)
Ci-dessous nous avons une structure JSON représentant notre session. Toutes les données fournies pour le moment dans l’image ci-dessous sont correctes. Une fois ces données envoyées au serveur et l’insertion effectuée avec succès nous recevons en retour la session avec un nouvel Id généré par notre serveur.
Nous pouvons modifier chacune des propriétés de la donnée JSON afin de casser certaines règles de validations de notre modèle. Si toutes les propriétés renseignées par l’utilisateur ne respectent pas les règles de validation alors voici la structure JSON que nous aurons en réponse de notre service REST :
Comme le montre l’image ci-dessus, nous avons en retour le code HTTP 400 Bad Request qui notifie au client l’incohérence de sa requête. La structure JSON reçue en réponse montre les différentes erreurs rencontrées et elle nous propose 2 propriétés :
- message : contenant un texte explicit sur l’état de la requête.
- modelState : un objet qui comportera autant de propriétés qu’il y a de propriétés de notre modèle impliquées dans l’invalidité de la requête. Chaque propriété de modelState représente un tableau de chaines de caractères où chaque chaîne est associée au message expliquant la règle non respectée.
Nous avons testé en saisissant des données mais rien ne nous oblige à en fournir. En mettant un point d’arrêt au niveau de notre action Post et en envoyant la requête sans rien mettre dans le corps voici ce qu’on a :
Notre service REST reçoit bien la requête. Notre modèle est null par contre, dans l’image ci-dessus, nous voyons que ModelState. IsValid est à true. Ce qui peut sembler bizarre vu que pas mal de propriétés de la classe Session sont au minimum requises alors pourquoi avec une instance null, notremodèle est-il considéré comme valide ? Pour cela il faudra voir comment la validation du modèle est effectuée. En explorant l’implémentation (grâce à l’outil JustDecompile) de la propriété IsValid de la classe ModelStateDictionary voici ce qu’on a :
La propriété IsValid considère que le modèle est valide si toutes les instances de type ModelState de la propriété Values (représentant les valeurs reçues) n’ont aucun contenu dans la collection Errors. Avec un modèle null, aucune valeur n’est reçue alors Values reste vide donc logique que l’instruction dans l’image ci-dessus renvoie true. Pour corriger ce problème on peut, dans l’implémentation de notre action Post, modifier le code comme suit :
public async Task<IHttpActionResult> Post(Session session)
{
try
{
if (session == null) return BadRequest();
if (!ModelState.IsValid) return BadRequest(ModelState);
// Traitement à effectuer
return Ok(session);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
Avec cette nouvelle condition, nous nous mettons à l’abri d’une exception de type NullReferenceException pour la suite.
Tout au long de l’article nous avons manipulé uniquement l’action Post. Bien que les règles de validations peuvent varier d’une action à l’autre (par exemple pour l’action Put, la propriété Id devra forcément être renseignée pour nous permettre d’identifier la session à modifier), les 2 blocs if dans le code précédent devront être repris dans pas mal d’implémentations où les données d’une session sont susceptibles d’être altérées. Cette répétition casse le principe DRY. Une solution est d’utiliser les filtres plus précisément les filtres d’action. Un filtre d’action est un attribut qui s’applique sur la méthode représentant notre action. Une fois appliqué cela nous permettra d’exécuter du code avant ou après l’exécution de l’action. En gros il suffit de mettre en place ce filtre d’action qui nous permettra de vérifier nos règles de validations. Pour voir comment créer ces attributs, je vous recommande cet article de Filip W. qui en fournit 2 : un pour détecter la validité de notre modèle et un autre pour éviter le fait que notre modèle soit null.
Conclusion :
Dans cette première partie nous avons vu comment mettre assez facilement des règles de validations au sein de notre modèle grâces aux attributs d’annotations tels que Required, StringLength etc… Cependant notre modèle ne prend pas en compte toutes nos règles de validations parce que rien n’empêche pour le moment à l’utilisateur de fournir une session ayant une heure de début supérieure à l’heure de fin. Il s’agit d’une logique de validation pas très complexe mais aucun des attributs d’annotations parmi ceux fournis par le Framework .Net ne permet de mettre cette règle sans passer par un peu plus de code au lieu du fait de placer uniquement un attribut sur une propriété. Pour faciliter cette tâche nous avons la possibilité d’étendre l’un des attributs déjà défini ou dériver directement de la classe de base ValidationAttribute. Ce sera le but de la deuxième partie où nous verrons les différentes solutions pour la mise en place d’une validation personnalisée que ce soit en utilisant l’attribut CustomAttribute (voir le diagramme précédent), en dérivant d’un attribut ou en implémentant certains interfaces.