Проверка модели в веб-API ASP.NET

В этой статье показано, как отобразить модели, использовать заметки для проверки данных и обрабатывать ошибки проверки в веб-API. Когда клиент отправляет данные в веб-API, часто требуется проверить данные перед выполнением любой обработки.

Заметки к данным

В ASP.NET веб-API можно использовать атрибуты из пространства имен System.ComponentModel.DataAnnotations для задания правил проверки свойств в модели. Рассмотрим следующую модель :

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; }
    }
}

Если вы использовали проверку модели в ASP.NET MVC, это должно выглядеть знакомо. Обязательный атрибут говорит, что Name свойство не должно иметь значение NULL. Атрибут Range говорит, что Weight должно быть от нуля до 999.

Предположим, что клиент отправляет запрос POST со следующим представлением JSON:

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

Вы увидите, что клиент не включал Name свойство, которое помечено как обязательное. Когда веб-API преобразует JSON в экземпляр Product, он проверяет соответствие Product атрибутам проверки. В действии контроллера можно проверить, является ли модель допустимой:

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);
            }
        }
    }
}

Проверка модели не гарантирует, что клиентские данные безопасны. Для других слоев приложения может потребоваться дополнительная проверка. (Например, уровень данных может применять ограничения внешнего ключа.) В руководстве по использованию веб-API с Entity Framework рассматриваются некоторые из этих проблем.

"Недопубликация": Недопубликация возникает, когда клиент пропускает некоторые свойства. Например, предположим, что клиент отправляет следующее:

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

Здесь клиент не указал значения для Price или Weight. Модуль форматирования JSON назначает значение по умолчанию нулю отсутствующим свойствам.

Снимок экрана: фрагмент кода с параметрами раскрывающегося меню Product Store. Models. Product.

Состояние модели допустимо, так как ноль является допустимым значением для этих свойств. Является ли это проблемой, зависит от вашего сценария. Например, в операции обновления может потребоваться различать "ноль" и "не задано". Чтобы принудительно задать значение клиентам, сделайте свойство null и задайте атрибут Required :

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

"Избыточное размещение": клиент также может отправлять больше данных, чем вы ожидали. Рассмотрим пример.

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

Здесь JSON содержит свойство ("Цвет"), которое не существует в Product модели. В этом случае модуль форматирования JSON просто игнорирует это значение. (Модуль форматирования XML выполняет то же самое.) Чрезмерное размещение приводит к проблемам, если у модели есть свойства, которые вы намерены сделать доступными только для чтения. Рассмотрим пример.

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

Вы не хотите, чтобы пользователи обновляли IsAdmin свойство и повышали себя до администраторов! Самая безопасная стратегия — использовать класс модели, который точно соответствует тому, что клиент может отправлять:

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

Обработка ошибок проверки

Веб-API не возвращает клиенту ошибку при сбое проверки. На действии контроллера лежит обязанность проверять состояние модели и реагировать соответствующим образом.

Кроме того, можно создать фильтр действий для проверки состояния модели перед вызовом действия контроллера. В следующем коде показан пример:

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);
            }
        }
    }
}

Если проверка модели завершается ошибкой, этот фильтр возвращает HTTP-ответ, содержащий ошибки проверки. В этом случае действие контроллера не вызывается.

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

Чтобы применить этот фильтр ко всем контроллерам веб-API, добавьте экземпляр фильтра в коллекцию HttpConfiguration.Filters во время настройки:

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

        // ...
    }
}

Другим вариантом является установка фильтра в качестве атрибута для отдельных контроллеров или действий контроллера:

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