Validation du modèle dans l’API web ASP.NET

Cet article explique comment annoter vos modèles, utiliser les annotations pour la validation des données et gérer les erreurs de validation dans votre API web. Lorsqu’un client envoie des données à votre API web, vous souhaitez souvent valider les données avant d’effectuer un traitement.

Annotations de données

Dans API Web ASP.NET, vous pouvez utiliser des attributs de l’espace de noms System.ComponentModel.DataAnnotations pour définir des règles de validation pour les propriétés de votre modèle. Considérez le modèle suivant :

using System.ComponentModel.DataAnnotations;

namespace MyApi.Models
{
    public class Product
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
        public decimal Price { get; set; }
        [Range(0, 999)]
        public double Weight { get; set; }
    }
}

Si vous avez utilisé la validation de modèle dans ASP.NET MVC, cela doit vous sembler familier. L’attribut Required indique que la Name propriété ne doit pas être null. L’attribut Range indique que doit Weight être compris entre zéro et 999.

Supposons qu’un client envoie une requête POST avec la représentation JSON suivante :

{ "Id":4, "Price":2.99, "Weight":5 }

Vous pouvez voir que le client n’a pas inclus la Name propriété, qui est marquée comme obligatoire. Lorsque l’API web convertit le json en instance Product , elle valide le Product par rapport aux attributs de validation. Dans votre action de contrôleur, vous pouvez case activée si le modèle est valide :

using MyApi.Models;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace MyApi.Controllers
{
    public class ProductsController : ApiController
    {
        public HttpResponseMessage Post(Product product)
        {
            if (ModelState.IsValid)
            {
                // Do something with the product (not shown).

                return new HttpResponseMessage(HttpStatusCode.OK);
            }
            else
            {
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
            }
        }
    }
}

La validation du modèle ne garantit pas que les données clientes sont sécurisées. Une validation supplémentaire peut être nécessaire dans d’autres couches de l’application. (Par exemple, la couche de données peut appliquer des contraintes de clé étrangère.) Le didacticiel Utilisation de l’API web avec Entity Framework explore certains de ces problèmes.

« Sous-publication » : la sous-publication se produit lorsque le client exclut certaines propriétés. Par exemple, supposons que le client envoie les éléments suivants :

{"Id":4, "Name":"Gizmo"}

Ici, le client n’a pas spécifié de valeurs pour Price ou Weight. Le formateur JSON affecte une valeur par défaut de zéro aux propriétés manquantes.

Capture d’écran de l’extrait de code avec les options de menu déroulant dot Product Store Models dot Product.

L’état du modèle est valide, car zéro est une valeur valide pour ces propriétés. La question de savoir s’il s’agit d’un problème dépend de votre scénario. Par exemple, dans une opération de mise à jour, vous pouvez faire la distinction entre « zéro » et « non défini ». Pour forcer les clients à définir une valeur, rendez la propriété nullable et définissez l’attribut Required :

[Required]
public decimal? Price { get; set; }

« Sur-publication » : un client peut également envoyer plus de données que prévu. Par exemple :

{"Id":4, "Name":"Gizmo", "Color":"Blue"}

Ici, le json inclut une propriété (« Color ») qui n’existe pas dans le Product modèle. Dans ce cas, le formateur JSON ignore simplement cette valeur. (Le formateur XML fait de même.) La sur-publication entraîne des problèmes si votre modèle a des propriétés que vous aviez l’intention d’être en lecture seule. Par exemple :

public class UserProfile
{
    public string Name { get; set; }
    public Uri Blog { get; set; }
    public bool IsAdmin { get; set; }  // uh-oh!
}

Vous ne souhaitez pas que les utilisateurs mettent à jour la IsAdmin propriété et s’élèvent eux-mêmes aux administrateurs ! La stratégie la plus sûre consiste à utiliser une classe de modèle qui correspond exactement à ce que le client est autorisé à envoyer :

public class UserProfileDTO
{
    public string Name { get; set; }
    public Uri Blog { get; set; }
    // Leave out "IsAdmin"
}

Notes

Le billet de blog de Brad Wilson« Input Validation vs. Model Validation in ASP.NET MVC » contient une bonne discussion sur la sous-publication et la sur-publication. Bien que le billet porte sur ASP.NET MVC 2, les problèmes restent pertinents pour l’API web.

Gestion des erreurs de validation

L’API web ne retourne pas automatiquement d’erreur au client en cas d’échec de la validation. Il appartient à l’action du contrôleur de case activée l’état du modèle et de répondre de manière appropriée.

Vous pouvez également créer un filtre d’action pour case activée l’état du modèle avant l’appel de l’action du contrôleur. Le code suivant montre un exemple :

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.ModelBinding;

namespace MyApi.Filters
{
    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }
}

Si la validation du modèle échoue, ce filtre retourne une réponse HTTP qui contient les erreurs de validation. Dans ce cas, l’action du contrôleur n’est pas appelée.

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Tue, 16 Jul 2013 21:02:29 GMT
Content-Length: 331

{
  "Message": "The request is invalid.",
  "ModelState": {
    "product": [
      "Required property 'Name' not found in JSON. Path '', line 1, position 17."
    ],
    "product.Name": [
      "The Name field is required."
    ],
    "product.Weight": [
      "The field Weight must be between 0 and 999."
    ]
  }
}

Pour appliquer ce filtre à tous les contrôleurs d’API web, ajoutez une instance du filtre à la collection HttpConfiguration.Filters pendant la configuration :

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ValidateModelAttribute());

        // ...
    }
}

Une autre option consiste à définir le filtre en tant qu’attribut sur des contrôleurs individuels ou des actions de contrôleur :

public class ProductsController : ApiController
{
    [ValidateModel]
    public HttpResponseMessage Post(Product product)
    {
        // ...
    }
}