ASP.NET Web API中的内容协商

本文介绍 ASP.NET Web API如何实现 ASP.NET 4.x 的内容协商。

HTTP 规范 (RFC 2616) 将内容协商定义为“当有多个表示形式可用时,为给定响应选择最佳表示形式的过程”。HTTP 中内容协商的主要机制是以下请求标头:

  • 接受: 响应可接受的媒体类型,例如“application/json”、“application/xml”或自定义媒体类型(如“application/vnd.example+xml” )
  • Accept-Charset: 哪些字符集是可接受的,例如 UTF-8 或 ISO 8859-1。
  • Accept-Encoding: 哪些内容编码是可接受的,例如 gzip。
  • Accept-Language: 首选的自然语言,例如“en-us”。

服务器还可以查看 HTTP 请求的其他部分。 例如,如果请求包含指示 AJAX 请求的 X-Requested-With 标头,则如果没有 Accept 标头,服务器可能默认为 JSON。

本文介绍 Web API 如何使用 Accept 和 Accept-Charset 标头。 (目前,没有对 Accept-Encoding 或 Accept-Language.)

序列化

如果 Web API 控制器以 CLR 类型返回资源,则管道将序列化返回值并将其写入 HTTP 响应正文。

例如,请考虑以下控制器操作:

public Product GetProduct(int id)
{
    var item = _products.FirstOrDefault(p => p.ID == id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return item; 
}

客户端可能会发送此 HTTP 请求:

GET http://localhost.:21069/api/products/1 HTTP/1.1
Host: localhost.:21069
Accept: application/json, text/javascript, */*; q=0.01

作为响应,服务器可能会发送:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 57
Connection: Close

{"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}

在此示例中,客户端请求 JSON、Javascript 或“任何内容” (*/*) 。 服务器使用 对象的 JSON 表示形式 Product 进行响应。 请注意,响应中的 Content-Type 标头设置为“application/json”。

控制器还可以返回 HttpResponseMessage 对象。 若要为响应正文指定 CLR 对象,请调用 CreateResponse 扩展方法:

public HttpResponseMessage GetProduct(int id)
{
    var item = _products.FirstOrDefault(p => p.ID == id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return Request.CreateResponse(HttpStatusCode.OK, item);
}

使用此选项可以更好地控制响应的详细信息。 可以设置状态代码、添加 HTTP 标头等。

序列化资源的对象称为 媒体格式化程序。 媒体格式化程序派生自 MediaTypeFormatter 类。 Web API 为 XML 和 JSON 提供媒体格式化程序,你可以创建自定义格式化程序以支持其他媒体类型。 有关编写自定义格式化程序的信息,请参阅 媒体格式化程序

内容协商的工作原理

首先,管道从 HttpConfiguration 对象获取 IContentNegotiator 服务。 它还从 HttpConfiguration.Formatters 集合中获取媒体格式化程序的列表。

接下来,管道调用 IContentNegotiator.Negotiate,传入:

  • 要序列化的对象的类型
  • 媒体格式化程序集合
  • HTTP 请求

Negotiate 方法返回两条信息:

  • 要使用的格式化程序
  • 响应的媒体类型

如果未找到格式化程序,则 Negotiate 方法返回 null,并且客户端收到 HTTP 错误 406 (“不可接受) ”。

以下代码演示控制器如何直接调用内容协商:

public HttpResponseMessage GetProduct(int id)
{
    var product = new Product() 
        { Id = id, Name = "Gizmo", Category = "Widgets", Price = 1.99M };

    IContentNegotiator negotiator = this.Configuration.Services.GetContentNegotiator();

    ContentNegotiationResult result = negotiator.Negotiate(
        typeof(Product), this.Request, this.Configuration.Formatters);
    if (result == null)
    {
        var response = new HttpResponseMessage(HttpStatusCode.NotAcceptable);
        throw new HttpResponseException(response));
    }

    return new HttpResponseMessage()
    {
        Content = new ObjectContent<Product>(
            product,		        // What we are serializing 
            result.Formatter,           // The media formatter
            result.MediaType.MediaType  // The MIME type
        )
    };
}

此代码等效于管道自动执行的操作。

默认内容协商程序

DefaultContentNegotiator 类提供 IContentNegotiator 的默认实现。 它使用多个条件来选择格式化程序。

首先,格式化程序必须能够序列化类型。 这可以通过调用 MediaTypeFormatter.CanWriteType 进行验证。

接下来,内容协商程序会查看每个格式化程序,并评估它与 HTTP 请求的匹配程度。 为了评估匹配项,内容协商程序在格式化程序上查看两项内容:

  • SupportedMediaTypes 集合,其中包含受支持媒体类型的列表。 内容协商程序尝试将此列表与请求 Accept 标头匹配。 请注意,Accept 标头可以包含范围。 例如,“text/plain”是 text/* 或 */*的匹配项。
  • MediaTypeMappings 集合,其中包含 MediaTypeMapping 对象的列表。 MediaTypeMapping 类提供一种通用方法,用于将 HTTP 请求与媒体类型匹配。 例如,它可以将自定义 HTTP 标头映射到特定媒体类型。

如果有多个匹配项,则具有最高质量因素的匹配将获胜。 例如:

Accept: application/json, application/xml; q=0.9, */*; q=0.1

在此示例中,application/json 的隐含质量系数为 1.0,因此它优先于 application/xml。

如果未找到匹配项,则内容协商程序会尝试在请求正文的媒体类型(如果有)上匹配。 例如,如果请求包含 JSON 数据,则内容协商程序会查找 JSON 格式化程序。

如果仍然没有匹配项,则内容协商程序只需选取可序列化该类型的第一个格式化程序。

选择字符编码

选择格式化程序后,内容协商程序会通过查看格式化程序上的 SupportedEncodings 属性来选择最佳字符编码,并将其与请求 (Accept-Charset标头(如果有任何) )进行匹配。