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 メソッドは、次の 2 つの情報を返します。

  • 使用するフォーマッタ
  • 応答のメディア タイプ

フォーマッタが見つからない場合、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 要求とどの程度一致するかを評価します。 一致を評価するため、コンテンツ ネゴシエーターはフォーマッタの 2 つの部分を調べます。

  • 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 ヘッダー (存在する場合) と照合して、最適な文字エンコードを選びます。