ASP.NET Web API 中的模型验证

本文介绍如何在 Web API 中批注模型、使用批注进行数据验证以及处理验证错误。 当客户端将数据发送到 Web API 时,通常需要先验证数据,然后再执行任何处理。

数据注释

在 ASP.NET Web 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 中使用过模型验证,这看起来应该很熟悉。 Required 属性指示 属性Name不能为 null。 Range 属性显示,Weight必须介于 0 和 999 之间。

假设客户端发送具有以下 JSON 表示形式的 POST 请求:

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

可以看到客户端不包含 Name 属性,该属性被标记为必需。 当 Web 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);
            }
        }
    }
}

模型验证不保证客户端数据安全。 在应用程序的其他层中可能需要其他验证。 (例如,数据层可能会强制实施外键约束。) 教程 将 Web API 与实体框架配合使用 探讨了其中一些问题。

“过帐不足”:当客户端遗漏某些属性时,将发生过低过帐。 例如,假设客户端发送以下内容:

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

此处,客户端未指定 PriceWeight的值。 JSON 格式化程序将默认值零分配给缺少的属性。

代码片段的屏幕截图,其中“产品商店”上带有“点模型”的下拉菜单选项。

模型状态有效,因为零是这些属性的有效值。 这是否是问题取决于你的方案。 例如,在更新操作中,可能需要区分“零”和“未设置”。若要强制客户端设置值,请将 属性设置为可以为 null 并设置 Required 属性:

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

“过度过帐”:客户端还可以发送比预期 更多的 数据。 例如:

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

此处,JSON 包含模型中不存在 Product 的属性 (“Color”) 。 在这种情况下,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"
}

注意

Brad Wilson 的博客文章“ASP.NET MVC 中的输入验证与模型验证”很好地讨论了发布不足和过度发布。 尽管该文章 ASP.NET MVC 2,但这些问题仍与 Web API 相关。

处理验证错误

验证失败时,Web 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."
    ]
  }
}

若要将此筛选器应用于所有 Web 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)
    {
        // ...
    }
}