ASP.NET MVC 開発者のためのコンテンツ ネゴシエーションと Web API
ASP.NET MVC に関して個人的に最も気に入っている特長の 1 つは、jQuery ベースのページ、モバイル アプリケーション、プレーンな C# バックエンドなどの HTTP クライアントから簡単に呼び出せる、メソッドのファサードを公開する機能です。長い間、このサービス層の構築は Windows Communication Foundation (WCF) サービスの領域で行われてきました。WCF を HTTP 専用にするための試みとして、webHttpBinding メカニズムが導入されたり、今では使用されなくなった REST Starter Kit などのフレームワークが導入されたりしました。しかし、いずれのアプローチも、悪名高い WCF の過剰構成、属性の乱用、テスト容易性を向上するよう設計されていない構造など、開発者にとっての障害を完全には取り除けませんでした。そこへ登場したのが Web API です。Web API は、単純かつテストが容易で、ホスティング環境 (IIS など) から独立し、HTTP に重点を置くように設計された新しいフレームワークです。
しかし、私見ながら、Web API のプログラミング インターフェイスは ASP.NET MVC に酷似しています。ASP.NET MVC のプログラミング インターフェイスは明確でよく定義されているので、けなしているわけではありません。実際、Web API は WCF のようなプログラミング モデルから出発し、ASP.NET MVC に似た形へと成長しました。
今回の記事では、まず平均的な ASP.NET MVC 開発者の視点から Web API の概要を紹介してから、プレーンな ASP.NET MVC に勝る利点を代表する Web API の機能領域、コンテンツ ネゴシエーションに注目します。
Web API の概要
Web API は、HTTP 要求を処理するクラスのライブラリ作成に利用できるフレームワークです。作成したライブラリを初期構成設定と共にランタイム環境にホストすると、呼び出し元から HTTP 経由で使用できます。コントローラー クラスのパブリック メソッドは、HTTP エンドポイントになります。構成可能なルーティング規則を使用すると、特定のメソッドへのアクセスに使用する URL の形式を定義しやすくなります。しかし、ルーティングの例外があるため、Web API での URL 処理の既定の形式は、大半の場合は構成ではなく規則によって決まります。
ASP.NET MVC 開発者の方は、ここで記事を読むのをやめて、ASP.NET MVC に "既に存在する" コントローラーの概念を模倣しただけのような新しいフレームワークを使う必要はないのではないかと思われるかもしれません。
端的にお答えすると、そのとおりです。プレーンなコントローラーでもほとんど同じ機能を実現できるので、ASP.NET MVC におそらく Web API は必要ありません。たとえば、JSON や XML 文字列形式のデータを簡単に返せます。バイナリ データやプレーン テキストを返すことも簡単で、好みの形に URL テンプレートを整えることもできます。
同一のコントローラー クラスで JSON データと HTML ビューを提供できますが、HTML を返すコントローラーとデータだけを返すコントローラーを分離することも簡単です。実際、プロジェクトに ApiController クラスを配置して、プレーン データを返すと想定しているすべてのエンドポイントをこのクラスに集約することは、一般的な手法です。以下に例を示します。
public class ApiController : Controller {
public ActionResult Customers()
{
var data = _repository.GetAllCustomers();
return Json(data, JsonRequestBehavior.AllowGet); }
…
}
Web API は ASP.NET MVC アーキテクチャを最大限に活用して、ASP.NET MVC の 2 つの主要領域を強化しています。1 つ目の強化点は、コンテンツ ネゴシエーションと呼ばれる新しい論理層の導入です。コンテンツ ネゴシエーションでは、JSON、XML など、特定の形式のデータを要求する標準的な規則を使用します。2 つ目の強化点は、Web API には ASP.NET や IIS との依存関係がまったくないことです。厳密には、system.web.dll ライブラリとの依存関係がありません。確かに、Web API は IIS に配置している ASP.NET アプリケーションでホストでき、これが今後もおそらく最も一般的なシナリオでしょう。しかし、Windows サービス、Windows Presentation Foundation (WPF) アプリケーション、コンソール アプリケーションなど、アドホックなホスティング環境を提供する他のアプリケーションでも、Web API ライブラリはホスト可能です。
加えて、ASP.NET MVC に精通している開発者は、コントローラー、モデル バインディング、ルーティング、アクション フィルターなどの Web API の概念に違和感はないはずです。
Web フォーム開発者が Web API を好む理由
ASP.NET MVC 開発者は、ASP.NET MVC とほとんど同じプログラミング モデルを持つ Web API のメリットに関して、最初は困惑するかもしれません。しかし、Web フォーム開発者なら困惑することはないはずです。Web API を使えば、Web フォーム アプリケーション内から簡単に HTTP エンドポイントを公開できます。そのためには、次のようなクラスを 1 つ以上追加するだけです。。
public class ValuesController : ApiController
{
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
public string Get(int id)
{
return "value";
}
}
これは、ASP.NET MVC アプリケーションに Web API コントローラーを追加する際のコードと同じです。また、ルートを指定する必要もあります。アプリケーションの起動時に実行するコードを次に示します。
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional });
このクラスのパブリック メソッドは、NonAction 属性で特に注釈を付けない限り、既定の名前付け規則とルーティング規則に従っていれば HTTP で呼び出せるパブリック エンドポイントです。このようなメソッドは、生成したプロキシ クラス、web.config 参照、または特別なコードを必要としなくても、すべてのクライアントから呼び出せます。
Web API のルーティング規則によると、URL の先頭には /api を指定し、次にコントローラー名を指定します。アクション名は明確には規定されていません。アクションは要求の種類 (GET、PUT、POST、または DELETE) で決まります。Get、Put、Post、または Delete で始まるメソッド名は、対応するアクションに規則に従ってマップされます。たとえば、TaskController の GetTasks メソッドは、/api/task のような URL に対する GET 要求のために呼び出されます。
Web API は、動作やクラス名こそ ASP.NET MVC とよく似ていますが、まったく別のアセンブリのセットに所属し、まったく異なる型のセットを使用します (System.Net.Http がプライマリ アセンブリです)。
Web API コンテンツ ネゴシエーションの内部
一般に "コンテンツ ネゴシエーション" という用語は、受信 HTTP 要求の構造を検査してクライアントが予期している応答の形式を特定する処理を表します。技術的には、コンテンツ ネゴシエーションとはクライアントとサーバーの対話を通じて、できる限り最高の表現形式を決定する処理です。要求の検査とは、通常は Accept や Content-Type など、いくつかの HTTP ヘッダーを調べることです。特に Content-Type は、サーバーでは POST 要求と PUT 要求の処理に使用し、クライアントでは HTTP 応答のフォーマッタの選択に使用します。GET 要求には Content-Type を使用しません。
ただし、コンテンツ ネゴシエーションの内部のメカニズムはさらに高度です。前述のシナリオは、既定の規則と実装を使用しているごく典型的なシナリオですが、他のシナリオも可能です。
Web API のネゴシエーション処理を制御するコンポーネントは、DefaultContentNegotiator というクラスです。DefaultContentNegotiator はパブリック インターフェイス (IContentNegotiator) を実装しているため、必要に応じてネゴシエーター全体を置き換えることができます。既定のネゴシエーターの内部では、理想的な応答形式を特定するために、いくつかの条件を適用します。
ネゴシエーターは、登録済みメディア タイプ フォーマッタ (実際にオブジェクトを特定の形式に変換するコンポーネント) のリストを処理します。フォーマッタのリストを順に照合していき、最初に一致したフォーマッタで照合を中止します。フォーマッタからネゴシエーターに、現在の要求に対する応答をシリアル化できることを通知する方法は、いくつかあります。
最初のチェックは、MediaTypeMappings コレクションのコンテンツで実行します。このコレクションは、事前定義されたすべてのメディア タイプ フォーマッタでは、既定で空になっています。メディア タイプ マッピングは、現在の要求に対する応答をフォーマッタがシリアル化するために、満たす必要がある条件を表します。いくつかのメディア タイプ マッピングは事前に定義されています。その 1 つは、クエリ文字列の特定のパラメーターを調査します。たとえば、Web API 呼び出しに使用するクエリ文字列に xml=true 式が追加されるようにするだけで、XML のシリアル化を有効にできます。そのためには、カスタム XML メディア タイプ フォーマッタのコンストラクターに次のコードを記述します。
MediaTypeMappings.Add(new QueryStringMapping("xml", "true", "text/xml"));
同様に、拡張子をURL に追加したりカスタム HTTP ヘッダーを追加したりすると、呼び出し元の優先設定を通知できます。
MediaTypeMappings.Add(new UriPathExtensionMapping("xml", "text/xml"));
MediaTypeMappings.Add(new RequestHeaderMapping("xml", "true",
StringComparison.InvariantCultureIgnoreCase, false,"text/xml"));
URL パス拡張子の場合は、次の形式の URL が XML フォーマッタにマップされます。
http://server/api/news.xml
URL パス拡張子が機能するには、次のようなアドホック ルートを記述する必要があります。
config.Routes.MapHttpRoute(
name: "Url extension",
routeTemplate: "api/{controller}/{action}.{ext}/{id}",
defaults: new { id = RouteParameter.Optional }
);
カスタム HTTP ヘッダーの場合、RequestHeaderMapping クラスのコンストラクターは、ヘッダー名、想定される値、およびいくつかの追加パラメーターを受け取ります。あるオプション パラメーターは望ましい文字列比較モードを示し、別のオプション パラメーターは比較対象が文字列全体かどうかを表すブール値です。ネゴシエーターは、メディア タイプ マッピング情報を使用しても一致するフォーマッタが見つからない場合は、Accept や Content-Type などの標準的な HTTP ヘッダーを調査します。何も一致しない場合は、登録済みフォーマッタのリストを再照合し、リスト内のいずれかのフォーマッタで要求の戻り値の型をシリアル化できるかどうかチェックします。
カスタム フォーマッタを追加するには、アプリケーションの起動処理 (たとえば Application_Start メソッド) に次のようなコードを挿入します。
config.Formatters.Add(xmlIndex, new NewsXmlFormatter());
ネゴシエーション処理をカスタマイズする
ほとんどの場合は、メディア タイプ マッピングを使用すれば簡単にシリアル化に関する特別な要件を満たせます。しかし、次のように派生クラスを記述して MatchRequestMediaType メソッドをオーバーライドすると、既定のコンテンツ ネゴシエーターをいつでも置き換えることができます。
protected override MediaTypeFormatterMatch MatchRequestMediaType(
HttpRequestMessage request, MediaTypeFormatter formatter)
{
...
}
IContentNegotiator インターフェイスを実装する新しいクラスを使用すれば、完全にカスタマイズしたコンテンツ ネゴシエーターを作成できます。自作のネゴシエーターが完成したら、Web API ランタイムに登録します。
GlobalConfiguration.Configuration.Services.Replace(
typeof(IContentNegotiator),
new YourOwnNegotiator());
通常、このコードは global.asax に記述するか、Visual Studio によって ASP.NET MVC Web API プロジェクト テンプレートに作成される便利な構成ハンドラーのいずれかに記述します。
クライアントからコンテンツ形式を制御する
Web API でのコンテンツ ネゴシエーションの最も一般的なシナリオは、Accept ヘッダーの使用です。このアプローチを採用すると、コンテンツの形式が Web API コードに対して完全に透過的になります。呼び出し元で Accept ヘッダーを適切に (たとえば text/xml に) 設定すれば、Web API インフラストラクチャでそれに応じた処理が実行されます。次のコードは、Web API エンドポイントに対する jQuery 呼び出しで、XML を取得するよう Accept ヘッダーを設定する方法を示しています。
$.ajax({
url: "/api/news/all",
type: "GET",
headers: { Accept: "text/xml; charset=utf-8" }
});
C# コードの場合は、次のように Accept ヘッダーを設定します。
var client = new HttpClient();
client.Headers.Add("Accept", "text/xml; charset=utf-8");
HTTP ヘッダーの設定には、任意のプログラミング環境の任意の HTTP API を使用できます。HTTP ヘッダーを設定すると呼び出し元で問題が発生しそうな場合のベスト プラクティスは、メディア タイプ マッピングも追加して、コンテンツ形式に必要なすべての情報を URL に含めることです。
HTTP 要求の構造に応じて応答が大きく変わることに注意してください。Web API の URL をそれぞれ Internet Explorer 10 と Chrome のアドレス バーから要求してみると、一方では JSON が表示され、もう一方では XML が表示されますが、驚くことではありません。既定の Accept ヘッダーがブラウザーによって異なる場合があるためです。一般に、サードパーティが使用できるように API を公開する場合は、出力形式を選択する URL ベースのメカニズムを組み込む必要があります。
Web API を使用するシナリオ
アーキテクチャに関して、Web API は大きく進歩しました。先日 Open Web Interface for .NET (OWIN) NuGet パッケージ (Microsoft.AspNet.WebApi.Owin) と Katana プロジェクトが登場したことで、Web API の重要性はますます高まっています。Katana プロジェクトは、標準的な一連のインターフェイスを通じて、外部アプリケーションでの API のホスティングを簡略化します。ASP.NET MVC アプリケーション以外のソリューションを構築している場合、Web API を簡単に使用できます。しかし、Web API を ASP.NET MVC ベースの Web ソリューションで使用することには、どのような意味があるでしょうか。
プレーンな ASP.NET MVC を使用していれば、新しい技術を学ばなくても簡単に HTTP ファサードを構築できます。単純なコードをコントローラー基本クラス内やそのコードが必要なメソッド内に記述する (またはネゴシエートした ActionResult を作成する) だけで、非常に簡単にコンテンツ ネゴシエーションを実現できます。必要な処理は、アクション メソッドのシグネチャに追加パラメーターを設定してチェックし、パラメーター値に応じて応答を XML や JSON にシリアル化することだけです。XML または JSON のみを使用している限り、この手法は実用的です。しかし、他の形式にも対応する場合は、おそらく Web API を使用する必要があります。
既に述べたように、Web API は IIS の外部 (Windows サービスなど) でホストできます。もちろん、API を ASP.NET MVC アプリケーション内に配置している場合は、IIS でしかホストできません。そのため、作成する API 層の目標によってホスティングの種類は異なります。関連する ASP.NET MVC のサイトのみで API 層を使用する場合、おそらく Web API は必要ありません。作成する API 層の実態が、なんらかのビジネス コンテキストの API を公開する "サービス" の場合、ASP.NET MVC で Web API を使用することは理にかなっています。
Dino Esposito は、『Architecting Mobile Solutions for the Enterprise』(Microsoft Press、2012 年)、および近日出版予定の『Programming ASP.NET MVC 5』(Microsoft Press) の著者です。JetBrains の .NET および Android プラットフォームのテクニカル エバンジェリストでもあります。世界各国で開催される業界のイベントで頻繁に講演しており、software2cents.wordpress.com (英語) や Twitter (twitter.com/despos、英語) でソフトウェアに関するビジョンを紹介しています。
この記事のレビューに協力してくれた技術スタッフの Howard Dierking (マイクロソフト) に心より感謝いたします。
Howard Dierking は Windows Azure フレームワークとツール チームのプログラム マネージャーを務めており、ASP.NET、NuGet、および Web API に重点的に取り組んでいます。以前には、MSDN マガジンの編集長の経験や、Microsoft Learning のデベロッパー認定プログラムを指揮したことがあります。マイクロソフトに勤務する前は、分散システム専門の開発者兼アプリケーション アーキテクトとして 10 年間仕事に取り組んでいました。