miso_soup3 Blog

主に ASP.NET 関連について書いています。

ASP.NET Web API 取得するデータの書式を指定する ~Content Negotiation~

ASP.NET Web API は、デフォルトで Json, Xml, FormUrlEncoded の書式に対応しています。

データを取得する際は、リクエストのヘッダー Accept 値で、どの書式でデータを取得するか指定できます。
また、コードを数行追加すれば、「~/api/movies.json/」のように URL で指定したり、
QueryString「~/api/movies/all?format=json」、カスタムヘッダー値で指定できたりします。

実際にやってみます。
下のように「~/values/」で Person データを取得する API を用意します。

public class ValuesController : ApiController
{
	public Person Get()
	{
		var person = new Person() { Id = 1, Name = "Taro" };
		return person;
	}
}

public class Person
{
	public int Id { get; set; }
	public String Name { get; set; }
}

※ FormUrlEncoded は送信用の書式なので、Xml と Json で試します。

Accept 値で指定

Json の場合は、application/json、
Xml の場合は、application/xml を、 Accept で指定します。

URL の一部で指定

URL「~/api/values.json/」でリクエストすると、Json で、
URL「~/api/values.xml/」でリクエストすると、Xml で取得できるようにします。
これには、少しコードの追加が必要です。

WebApiConfig.cs にて、ルーティングの設定と、MediaTypeMapping の設定を追加します。
(MediaTypeMapping が何者かについては後で記述します)

// ※using System.Net.Http.Formatting; が必要です。

public static class WebApiConfig
{
	public static void Register(HttpConfiguration config)
	{
		config.Formatters.XmlFormatter.AddUriPathExtensionMapping("xml", "application/xml");
		config.Formatters.JsonFormatter.AddUriPathExtensionMapping("json", "application/json");
		
		config.Routes.MapHttpRoute(
			name: "DefaultApi",
			routeTemplate: "api/{controller}.{ext}/{id}",
			defaults: new { id = RouteParameter.Optional }
		);
		....

デフォルトのルーティングに、ルートデータ"ext"を追加しています。(必ず ext にする必要があります。)
また、AddUriPathExtensionMappin メソッドにて、XmlFormatter と JsonFormatter に、
"ext"の位置に指定する値(第1引数)と、
どの MediaTypeFormatter を使用するか(第2引数:文字列)を指定しています。
(どの MediaTypeFormatter を使用するか = どの書式でデータを表すか)

※最後のスラッシュを省いて(~/api/values.xml)アクセスすると、404 が返ってきます。
ここでは省略しますが、"api/{controller}/{id}.{ext}"等に対応する場合は、もうちょっとルーティングの設定が必要です。

QueryString で指定

URL「~/api/values?format=xml」でリクエストすると、Json で、
URL「~/api/values?format=json」でリクエストすると、Xml で取得できるようにします。
これも、先と同じようにコードの追加が必要です。

WebApiConfig.cs に下のようにコードを追加します。

public static class WebApiConfig
{
	public static void Register(HttpConfiguration config)
	{
		config.Formatters.XmlFormatter.AddQueryStringMapping("format", "xml", "application/xml");
		config.Formatters.JsonFormatter.AddQueryStringMapping("format", "json", "application/json");
		...
		

AddQueryStringMapping メソッドを使用しています。
引数は、左から、QueryString の名前、値、どの MediaTypeFormatter を使用するか、を指定します。

カスタムヘッダー値で指定

任意のカスタムヘッダー値で指定することもできます。
例えば、「My-Header-Format」に Json が指定されていれば、Json で、
Xml が指定されていれば、Xml で取得できるようにします。

これも同様に WebApiConfig.cs にコードを追加します。

public static class WebApiConfig
{
	public static void Register(HttpConfiguration config)
	{
		config.Formatters.XmlFormatter.AddRequestHeaderMapping("My-Header-Format", "xml", StringComparison.CurrentCultureIgnoreCase, false, "application/xml");
		config.Formatters.JsonFormatter.AddRequestHeaderMapping("My-Header-Format", "json", StringComparison.CurrentCultureIgnoreCase, false, "application/json");
		...
		

今度は、AddRequestHeaderMapping を使用しています。
第3引数の bool は、部分一致とするかどうかの指定です。

以上、デフォルトで用意されているものを使って、
リクエストの値によって、取得するデータの書式を指定する方法でした。

これより下は、その仕組みについてですが…少し細かいです。

仕組み ― Content Negotiation

これらの動作は、Content Negotiation の機能によって働いています。

ContentNegotiation は何者かというと、凄く簡単にいえば、”どうやってデータを表すか決定する人”です。
一方、MediaTypeFormatter は、実際に自分の担当する書式に変換する人で、ContentNegotiation に従います。

壁に貼ってあるライフサイクルポスターで言うと、右下の「結果の変換」の部分です。
(え?貼ってないですか? → Get PDF

f:id:miso_soup3:20130705065250p:plain

この部分は、下のように流れます。

  1. ApiController の アクションメソッドの戻り値が、ある型である。(voidでもなく、HttpResponseMessageでもなく、IHttpActionResult でもなく。)
  2. この戻り値を、HTTP レスポンスの Body に入れてクライアントに返したいが、ある型のオブジェクトのままでは困る。
  3. ContentNegotiation が、そのオブジェクトを、どの MediaTypeFormatter を使って Body に格納すればいいか決める。
  4. 指定された MediaTypeFormatter がオブジェクトを自分の担当する書式へ変換する。
  5. 変換されたコンテンツは、HTTP レスポンス の Body に格納されて、クライアントへ。

で、この②の ContentNegotiation は、IContentNegotiation インターフェイスで定義されていて、
デフォルトでは、DefaultContentNegotiation が働いています。
(もちろん開発者がこの部分の処理を入れ替えることができる。)

では、このデフォルトの DefaultContentNegotiation はどのように MediaTypeFormatter を決定するのかというと…
以下の優先順位で決定されます。

A MediaTypeFormatter が 戻り値の型に対応しているかどうか?(CanWriteType)
B MediaTypeFormatter がもつ、MediaTypeMappings のルールできめる。
C MediaTypeFormatter が、HTTP リクエストの Accept 値と対応しているか。(SupportedMediaTypes)

特に何もしていないデフォルトの状態で json や xml とかの場合は、A と B の条件はスルーして、C の条件によって決められます。
これが、1つ目の例で動作した、Accept 値によって書式が決まる正体です。

2、3番目の例で紹介した、QueryString や、URL、任意のヘッダーの値で、というのは、
B の条件、MediaTypeMappings によるものです。
MediaTypeMappings は、ルールそのものを表すオブジェクトです。

先の優先順位があるので、例えば、Accept 値が Json で、2つめの例のように QueryString で Xml を指定した場合は、
Xml でデータが返されます。

まとめ

データを取得する際は、リクエストのヘッダー Accept 値で、どの書式でデータを取得するか指定できます。
また、コードを数行追加すれば、URL、QueryString、カスタムヘッダー値でも指定できます。

ContentNegotiation は”どうやってデータを表すか決定する人”で、
MediaTypeMappings は、そのルールの一部、
MediaTypeFormatter は、”実際に自分の担当する書式に変換する人”です。