ASP.NET Web API 中的內容協商

本文介紹 ASP.NET Web API 如何實作 ASP.NET 4.x 的內容協商。

HTTP 規範 (RFC 2616) 將內容協商定義為「當有多種表示法可用時,為指定回應選擇最佳表示的過程」。HTTP 中內容協商的主要機制是以下請求標頭:

  • Accept:指定可接受的回應媒體類型,例如「application/json」、「application/xml」或自訂媒體類型 (例如「application/vnd.example+xml」)
  • Accept-Charset:指定可接受的字元集,例如 UTF-8 或 ISO 8859-1。
  • Accept-Encoding:指定可接受的內容編碼,例如 gzip。
  • Accept-Language:指定首選的自然語言,例如「en-us」。

伺服器也可以查看 HTTP 請求的其他部分。 例如,如果請求包含 X-Requested-With 標頭 (指示 AJAX 請求),則如果沒有 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 或「任何內容」(*/*)。 伺服器以 Product 物件的 JSON 表示形式進行回應。 請注意,回應中的 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 標頭 (如果有) 進行匹配來選擇最佳字元編碼。