Validación de modelos en ASP.NET Web API

En este artículo se indica cómo anotar los modelos, usar las anotaciones para la validación de datos y controlar los errores de validación en la API web. Cuando un cliente envía datos a la API web, a menudo quiere validar los datos antes de realizar cualquier procesamiento.

Anotaciones de datos

En ASP.NET Web API, puede usar atributos del espacio de nombres System.ComponentModel.DataAnnotations para establecer reglas de validación para las propiedades del modelo. Considere el modelo siguiente:

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 ha usado la validación de modelos en MVC de ASP.NET, esto debería resultarle familiar. El atributo Required indica que la propiedad Name no debe ser null. El atributo Range indica que Weight debe estar entre 0 y 999.

Supongamos que un cliente envía una solicitud POST con la siguiente representación JSON:

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

Puede ver que el cliente no incluyó la propiedad Name, que está marcada como obligatoria. Cuando la API web convierte el JSON en una instancia de Product, valida Product comparándolo con los atributos de validación. En la acción del controlador, puede comprobar si el modelo es válido:

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 validación de modelos no garantiza que los datos del cliente sean seguros. Es posible que se necesite una validación adicional en otras capas de la aplicación. (Por ejemplo, la capa de datos podría aplicar restricciones de clave externa). El tutorial Uso de API web con Entity Framework explora algunos de estos problemas.

"Infrapublicación": la infrapublicación se produce cuando el cliente omite algunas propiedades. Por ejemplo, supongamos que el cliente envía lo siguiente:

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

Aquí, el cliente no especificó los valores de Price o Weight. El formateador JSON asigna un valor predeterminado de cero a las propiedades que faltan.

Screenshot of code snippet with Product Store dot Models dot Product's drop-down menu options over it.

El estado del modelo es válido, porque cero es un valor válido para estas propiedades. Dependerá de su escenario que esto sea un problema. Por ejemplo, en una operación de actualización, es posible que desee distinguir entre "cero" y "no establecido". Para forzar a los clientes a establecer un valor, haga que la propiedad admita un valor NULL y establezca el atributo Required:

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

"Publicación en exceso": un cliente también puede enviar más datos de lo esperado. Por ejemplo:

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

Aquí, el JSON incluye una propiedad ("Color") que no existe en el modelo Product. En este caso, el formateador JSON simplemente omite este valor. (El formateador XML hace lo mismo). La publicación excesiva provoca problemas si el modelo tiene propiedades que pretende que sean de solo lectura. Por ejemplo:

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

No quiere que los usuarios puedan actualizar la propiedad IsAdmin y sus privilegios se eleven a los de los administradores. La estrategia más segura es usar una clase de modelo que coincida exactamente con lo que el cliente puede enviar:

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

Nota:

La entrada de blog de Brad Wilson "Validación de entradas en comparación con la validación de modelos en MVC de ASP.NET" incluye un buen análisis sobre la infrapublicación y la publicación en exceso. Aunque la publicación es sobre MVC de ASP.NET 2, los problemas siguen siendo pertinentes para la API web.

Control de errores de validación

La API web no devuelve automáticamente un error al cliente cuando se produce un error en la validación. Es necesario que la acción del controlador compruebe el estado del modelo y responda adecuadamente.

También puede crear un filtro de acción para comprobar el estado del modelo antes de invocar la acción del controlador. El código siguiente muestra un ejemplo:

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 se produce un error en la validación del modelo, este filtro devuelve una respuesta HTTP que contiene los errores de validación. En ese caso, no se invoca la acción del controlador.

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."
    ]
  }
}

Para aplicar este filtro a todos los controladores de API web, agregue una instancia del filtro a la colección HttpConfiguration.Filters durante la configuración:

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

        // ...
    }
}

Otra opción es establecer el filtro como un atributo en controladores individuales o acciones de controlador:

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