July 2009
Volume 24 Number 07
RESTful XHTML - ASP.NET MVC による RESTful サービス
Aaron Skonnard | July 2009
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコード参照
この記事では、次の内容について説明します。
|
この記事では、次のテクノロジを使用しています。 REST、XHTML、ASP.NET |
目次
XHTML: データとリンクを表現する
XHTML: フォームによる入力を表現する
ASP.NET MVC のアーキテクチャを理解する
モデルを実装する
コントローラーを実装する
ルートを含む URI をデザインする
ビューを実装する
ブックマーク サービスを使用する
謝辞
RESTful サービスとは、プログラムからナビゲーションを行うことができるリソースの Web です。RESTful サービスをデザインするときは、この Web が機能するしくみを慎重に考えなければなりません。つまり、ナビゲーションを機能させるリンクを備えたリソース表現をデザインし、サービスへの入力方法を記述して、コンシューマーが実行時にサービス内をナビゲーションする方法を考えます。これらは見落とされがちですが、REST がもたらす可能性を最大限に引き出すうえで重要です。
現在人々は、HTML などの一般的な種類のコンテンツのレンダリング方法を認識している Web ブラウザーを利用して、サイト間を移動しています。HTML は、リソース間のリンクを確立し (<a> 要素)、アプリケーションの入力を記述および送信する (<form> 要素と <input> 要素) ための、構文やセマンティクスを提供します。
ユーザーが、レンダリングされたページの <a> 要素をクリックすると、ブラウザーがこれを認識して、目的のリソースに向けて HTTP GET 要求を発行し、その応答をレンダリングします。<form> 要素があれば、ブラウザーはそのフォームの説明を、ユーザーが入力して、GET または POST 要求を使用して送信できるユーザー インターフェイスにレンダリングします。ユーザーが送信ボタンを押すと、ブラウザーはデータをエンコードし、指定された要求を使用してデータを送信します。これらの 2 つの機能は、Web の成功に大きな役割を果たしています。
汎用の HTTP インターフェイスと連携するリンクを使用すると、クライアント コードを変更することなく実行時に、時間の経過とともに移動した新しい場所に要求をリダイレクトでき、セキュリティの特定の側面を変更できます。フォームの標準アプローチも同様に、クライアント コードを変更することなく、入力プロパティの追加と削除や、既定値の変更を行えます。いずれの機能も、時間の経過と共に進化するアプリケーションを構築するのに非常に有効です。
そのため、RESTful サービスでも、使用するリソース表現を通じて、これら 2 つの機能を提供することになります。たとえば、構築するサービス用に XML 言語の独自の亜流をデザインするのであれば、おそらく、リンクを確立し、サービスへの入力を記述し、Web を通じて利用者をガイドする独自の要素を用意することになります。さもなければ、単純に XHTML を使用します。
ほとんどの開発者は、"サービス" の選択肢として、すぐに XHTML を思い浮かべることはないでしょうが、実際には、これは XHTML の本来の使用方法の 1 つです。XHTML ドキュメントは、本質的に整形式の XML なので、標準の XML API を使用した自動処理が可能です。XHTML は HTML でもあるので、前述したリンクのナビゲーションやサービスへの入力をモデル化するために、<a>、<form>、および <input> の各要素を使用できます。一見して最初にやや変わっていると感じる唯一の点は、ユーザー定義のデータ構造をモデル化する方法です。ただし、<div> 要素と <span> 要素を使用してクラスやフィールドをモデル化し、<ol> 要素と <li> 要素を使用してエンティティのコレクションをモデル化することができます。こうした方法については、この記事の後半で詳しく説明します。
つまり、XHTML を RESTful サービスの既定表現と考える理由はいくつかあります。まず、独自の構文やセマンティクスを考案することはなく、<a>、<form>、<input> などの重要な要素の構文およびセマンティクスをそのまま利用できます。次に、サービスがユーザーからもアプリケーションからも閲覧可能になるため、最終的には、こうしたサービスがサイトのように感じられるようになります。XHTML は、依然として人間が解釈します。ただし、実行時にユーザーが解釈するのではなく、開発時にプログラマが解釈します。その結果、開発プロセス全体が簡素化され、利用者がサービスのしくみを理解しやすくなります。最後に、RESTful サービスの構築に、標準の Web 開発フレームワークを活用できます。
ASP.NET MVC は、こうした標準開発フレームワークの 1 つで、XHTML ベースのサービスを構築するための RESTful モデルが組み込まれています。この記事では、XHTML のデザイン概念をいくつか紹介し、完全な XHTML ベースの RESTful サービスを構築する方法を説明します。このサービスは、MSDN マガジンのサイトからダウンロードできます。
XHTML: データとリンクを表現する
ASP.NET MVC について詳しく説明する前に、まず、XHTML で一般的なデータ構造とデータのコレクションを表現する方法を見ておきましょう。ここで紹介する方法は、データの構造とコレクションを表現する唯一の方法というわけではありませんが、現在の XHTML ベースのサービスではかなり一般的な方法です。
ここでは、シンプルなブックマーク サービスを実装する方法を説明します。ユーザーはこのサービスを利用して、ブックマークを作成、取得、更新、および削除することができ、ブックマークを設定した Web にさまざまな方法でナビゲーションを行うことができます。以下のようにブックマークを表現する C# クラスがあるとします。
public class Bookmark
{
public int Id { get; set; }
public string Title { get; set; }
public string Url { get; set; }
public string User { get; set; }
}
最初の問題点は、XHTML で Bookmark のインスタンスを表現する方法です。1 つは、<div>、<span>、および <a> の各要素を組み合わせ、<div> 要素で構造を表し、<span> 要素でフィールドを表し、<a> 要素で他のリソースの ID とのリンクを表す方法があります。さらに、XHTML の "class" 属性を使用して、これらの要素に注釈を付けることで、追加の型メタデータを提供できます。次にこの例を示します。
<div class="bookmark">
<span class="bookmark-id">25</span>
<span class="bookmark-title">Aaron's Blog</span>
<a class="bookmark-url-link" href="http://pluralsight.com/aaron"
>http://pluralsight.com/aaron</a>
<span class="bookmark-username">skonnard</span>
</div>
次の問題点は、コンシューマーがこの情報を処理する方法です。これは整形式の XML なので、コンシューマーは任意の XML API を使用して、ブックマーク情報を抽出できます。.NET のプログラマであれば、XHTML をプログラムで使用する最も自然なプログラミング モデルが XLinq で提供されていることをご存知でしょう。さらに一歩進めて、プログラミング モデルをさらに簡単にするいくつかの便利な XHTML ベースの拡張メソッドを使用して、XLinq をさらに強化することもできます。
ここでは、一連の XLinq 拡張メソッドを使用します。これらの拡張メソッドは、ダウンロード可能なサンプル コードに含めてあります。これらの拡張機能により、実現可能なことについて、よいアイデアが浮かびます。以下のコードに、XLinq といくつかの拡張機能を使用して、先ほどのブックマークの XHTML を使用する方法を示します。
var bookmarkDetails = bookmarkDoc.Body().Struct("bookmark");
Console.WriteLine(bookmarkDetails["bookmark-id"].Value);
Console.WriteLine(bookmarkDetails["bookmark-url"].Value);
Console.WriteLine(bookmarkDetails["bookmark-title"].Value);
ここで、このブックマークのブラウザーでのレンダリング方法を改善したければ、カスケード スタイルシート (CSS) を追加してレンダリング時のブラウザー固有の詳細を制御するか、コンシューマーが必要なデータを抽出する機能を制限しない UI 要素やテキストをいくつか追加します。たとえば、以下のような XHTML であれば人間が容易に扱えるようになりますが、前述の .NET コード サンプルを変更しなくても、そのまま情報を処理できます。
<h1>Bookmark Details: 3</h1>
<div class="bookmark">
BookmarkID: <span class="bookmark-id">25</span><br />
Title: <span class="bookmark-title">Aaron's Blog</span><br />
Url: <a class="bookmark-url-link" href="http://pluralsight.com/aaron"
>http://pluralsight.com/aaron</a><br />
Username: <span class="bookmark-username">skonnard</span></a><br />
</div>
リソースのコレクションをモデル化するのも難しくありません。次のように、<ol>、<li>、および <a> の各要素を組み合わせて、ブックマークの一覧を表現できます。
<ol class="bookmark-list">
<li><a class="bookmark-link" href="/bookmarks/1">Pluralsight Home</ a></li>
<li><a class="bookmark-link" href="/bookmarks/2">Pluralsight On- Demand!</a></li>
<li><a class="bookmark-link" href="/bookmarks/3">Aaron's Blog</a></li>
<li><a class="bookmark-link" href="/bookmarks/4">Fritz's Blog</a></li>
<li><a class="bookmark-link" href="/bookmarks/5">Keith's Blog</a></li>
</ol>
以下のコードでは、このブックマーク一覧をコンソールに表示する方法を示しています。
var bookmarks = bookmarksDoc.Body().Ol("bookmark-list").Lis();
bookmarks.Each(bm => Console.WriteLine("{0}: {1}",
bm.Anchor().AnchorText, bm.Anchor().AnchorLink));
各 <li> 要素に、特定のブックマークにリンクする <a> 要素が含まれています。アンカー (a) 要素のいずれかに移動したら、上記のブックマークの詳細表現を取得することになります。このようにリソース間のリンクを定義するにつれて、サービスはリンクされたリソースの Web になっていきます。
ユーザーが Web ブラウザーを使用してリソース間を移動する方法はかなりわかりやすいですが、アプリケーションでこうした機能を利用する場合はどうでしょう。このブックマークを利用するアプリケーションのプログラムでは、目的のアンカー要素を見つけてから、"href" 属性で指定される URI を対象にした GET 要求を発行する必要があります。こうした細かい処理も、アンカー要素への移動をカプセル化する XLinq 拡張メソッドに委ねることができます。
以下のコードでは、ブックマーク一覧の最初のブックマークに移動してから、対象のブックマーク URL にナビゲーションする方法を示します。結果の XHTML はコンソールに表示されます。
var bookmarkDoc = bookmarks.First().Anchor().Navigate();
var bookmarkDetails = bookmarkDoc.Body().Struct("bookmark");
var targetDoc = bookmarkDetails.Anchor("bookmark-url-link").Navigate();
Console.WriteLine(targetDoc);
サービスをリソースの Web としてナビゲーションを行うコンシューマーの構築について考え始めると、より RESTful な方法を本格的に考え始めたことになります。
XHTML: フォームによる入力を表現する
さて、コンシューマーがシステム内で新しいブックマークの作成を希望しているとします。コンシューマーは WSDL なしで、送信するデータとその送信方法を、どのようにして認識すればよいのでしょう。答えは簡単です。XHTML フォームを使用します。
コンシューマーは、まず、URI に対して GET 要求を発行し、ブックマーク作成フォームを取得します。サービスは、次のようなフォームを返します。
<h1>Create Bookmark</h1>
<form action="/bookmark/create" class="create-bookmark-form" method="post">
<p>
<label for="bookmark-title">Title:</label><br />
<input id="bookmark-title" name="bookmark-title" type="text" value="" />
</p>
<p>
<label for="bookmark-url">Url:</label><br />
<input id="bookmark-url" name="bookmark-url" type="text" value="" />
</p>
<p><input type="submit" value="Create" name="submit" /></p>
</form>
このフォームは、新しいブックマークを作成するための HTTP POST 要求の作成方法を記述しています。このフォームでは、bookmark-title フィールドと bookmark-url フィールドに入力する必要があることを示しています。この例では、bookmark-id はブックマーク作成時に自動生成され、bookmark-username はログインしたユーザーの ID から取得されます。このフォームでは、送信する必要があるデータとその送信方法も示されます。
ブラウザーでこのフォームがレンダリングされると、人間であればフォームに入力して送信ボタンをクリックするだけで、新しいブックマークを作成できます。コンシューマーがアプリケーションの場合、基本的には、フォームをプログラムから送信することによって、同じことを行います。繰り返しになりますが、このプロセスは、次のようにフォームベースの拡張メソッドを使用すれば簡単に実行できます。
var createBookmarkForm = createBookmarkDoc.Body().Form("create-bookmark- form");
createBookmarkForm["bookmark-title"] = "Windows Live";
createBookmarkForm["bookmark-url"] = "https://live.com/";
createBookmarkForm.Submit();
このコードを実行すると、Submit メソッドで "action" URL を対象とした HTTP POST 要求が生成され、複数の入力フィールドがまとめて、URL でエンコードされる文字列に書式設定されます (application/x-www-form-urlencoded)。最終的には、ブラウザーを使用するのと変わりなく、新しいブックマークが作成されます。
現在のブラウザーは、フォームのメソッドに GET 要求と POST 要求のみをサポートしていますが、ブラウザー以外のコンシューマーを対象とするときは、PUT 要求や DELETE 要求をフォームの "メソッド" として指定することもできます。Submit 拡張メソッドは、指定する任意の HTTP メソッドに対して、同じようにうまく機能します。
ASP.NET MVC のアーキテクチャを理解する
ASP.NET MVC アーキテクチャは、長い歴史のある、有名なモデル-ビュー-コントローラー (MVC: Model-View-Controller) デザイン パターンに基づいています。図 1 に、ASP.NET MVC の各コンポーネントとその関係を示します。ASP.NET MVC では、他の MVC コンポーネントの前にルーティング エンジンが配置されます。このルーティング エンジンは、着信する HTTP 要求を受け取り、コントローラーのメソッドにルーティングします。ルーティング エンジンがルーティングに使用する一連のルートは、Global.asax で一元的に定義します。
図 1 ASP.NET MVC のアーキテクチャ
一元管理されるルートでは、URL のパターンと、コントローラーの特定のメソッドとその引数との間のマッピングを定義します。リンクを生成するときに、これらのルートを使用して、リンクを適切に生成します。これにより、開発プロセス全体をとおして、URL のデザインを 1 か所で簡単に変更できるようになります。
コントローラーは、着信した要求から情報を抽出し、ユーザーが定義するモデル層とやり取りします。モデル層はなんでもかまいません (Linq to SQL、ADO.NET Entity Framework、NHibernate など)。この層では、ビジネス ロジックを実行して、基になるデータベースと通信します。モデルが System.Web.Mvc 名前空間に含まれていない点に注目してください。コントローラーは、モデルとのやり取りを終えたら、ビューを作成して、(ビューが出力のレンダリング時に使用する) モデル データをビューに提供します。
ここからは、ASP.NET MVC のアーキテクチャを使用して完全なブックマーク サービスを実装するプロセスについて説明します。このサービスでは、複数のユーザーと、公開/非公開のブックマークをサポートします。ユーザーは、すべての公開ブックマークの参照、ユーザー名やタグに基づくブックマークへのフィルター適用、および非公開ブックマークの独自コレクションの完全な管理 (CRUD) が可能です。
まず、ASP.NET MVC プロジェクトを作成します。[プロジェクトの種類] ボックスの [Web] の一覧に、ASP.NET MVC Web アプリケーション プロジェクト テンプレートがあります。既定のプロジェクト テンプレートでは、初心者向け MVC アプリケーションのサンプルが提供されるため、F5 キーを押してこれを実際に試してみることができます。
このソリューション構造には、モデル、ビュー、およびコントローラー用のディレクトリがそれぞれあり、ここに各コンポーネントのコードを配置します。既定のテンプレートには、2 つのコントローラーが付属しています。1 つはユーザー アカウントを管理するコントローラー (AccountController)、もう 1 つはホーム ディレクトリへの要求をサポートするコントローラー (HomeController) です。どちらのコントローラーもブックマーク サービスで使用します。
モデルを実装する
最初に注目するのは、ブックマーク サービスのモデルです。ブックマーク情報を管理する 3 つのテーブル (Bookmark、Tag、および BookmarkTag) を含む、シンプルな SQL Server データベースを作成します (図 2 参照)。ひと目見れば内容がわかると思います。
図 2 ブックマーク サービスの Linq to SQL モデル
唯一の注意点は、この例で使用する、組み込みの ASP.NET のフォーム認証とメンバーシップのサービスです。これは、プロジェクトに付属する既定の AccountController で提供され、サービスのユーザー アカウントを管理します。したがって、ユーザー アカウント情報は別のデータベース (aspnetdb.mdf) に格納され、ブックマーク情報とは分離されます。ユーザー名だけが Bookmark テーブルに格納されます。
モデルでは、このデータベースを使用するビジネス オブジェクトとビジネス ロジックを提供します。この例について、Linq to SQL モデルを定義しました (図 2 参照)。このモデルは、BookmarksModel.dbml で定義し、BookmarksModel.designer.cs にある C# コードを生成します。ここでは、Bookmark、Tag、および BookmarkTag という名前のクラスを使用します。また、BookmarksModelDataContext クラスもありますが、これはエンティティのブートストラップです。
この時点で、Linq to SQL のクラスを MVC のモデル層として直接操作してもかまいません。または、さらに一歩進めて、高レベルのリポジトリ クラスを定義することもできます。このクラスでは、論理ビジネス操作を定義し、さらに基になるデータ操作の詳細からコントローラーおよびビューを切り離すこともできます。図 3 に、ブックマーク サービスで使用する BookmarksRepository クラスの定義を示します。
図 3 BookmarksRepository クラス
public class BookmarksRepository
{
// generated Linq to SQL DataContext class
BookmarksModelDataContext db = new BookmarksModelDataContext();
// query methods
public IQueryable<Bookmark> FindAllBookmarks() { ... }
public IQueryable<Bookmark> FindAllPublicBookmarks() { ... }
public IQueryable<Bookmark> FindBookmarksByUser(string username) { ... }
public IQueryable<Bookmark> FindPublicBookmarksByUser(string username) { ... }
public IQueryable<Bookmark> FindBookmarksByTag(string tag) { ... }
public Bookmark FindBookmarkById(int id) { ... }
public IQueryable<string> FindUsers(){ ... }
public IQueryable<Tag> FindTags() { ... }
public Tag FindTag(string tag) { ... }
// insert/delete methods
public void AddBookmark(Bookmark bm) { ... }
public void AddTag(Tag t) { ... }
public void AddTagForBookmark(string tagText, Bookmark bm) { ... }
public void DeleteBookmark(Bookmark bm) { ... }
// persistence
public void Save() { ... }
}
コントローラーを実装する
コントローラーは、HTTP 要求のライフ サイクルを管理するコードです。要求が着信すると、ASP.NET MVC のルーティング エンジンが (URL に基づいて) 使用するコントローラーを決定し、コントローラーの特定のメソッドを呼び出すことにより、要求をコントローラーにルーティングします。したがって、コントローラーを作成するときに、ルーティング エンジンから呼び出されるエントリ ポイントを作成します。
ブックマーク サービスでは、認証済みのユーザーがブックマークを作成、編集、および削除できます。ブックマークを作成するときは、ユーザーがそのブックマークを "公開 (共有)" または "非公開" としてマークすることが可能です。すべてのユーザーは、公開ブックマークを参照でき、ユーザー名やタグによってブックマークをフィルター選択できます。ただし、非公開のブックマークは、所有者だけが表示可能です。また、承認済みのコンシューマーは、特定のブックマークに関する詳細を取得できます。さらに、ユーザーやタグに関連付けられている公開ブックマークにナビゲートする方法として、システム内のすべてのユーザーとタグを参照できるようにします。
図 4 に、ここで説明したサービス要求をサポートする BookmarkController クラスに必要なメソッドを示します。最初の 3 つのクエリ メソッドでは、すべての公開ブックマーク、ユーザーによるブックマーク、またはタグによるブックマークを取得できるようにします。また、このクラスには、ユーザーとタグを取得するメソッド、特定の bookmark インスタンスの詳細を取得するメソッドもあります。これらすべてのメソッドが HTTP GET 要求に応答しますが、各メソッドは、ルートを定義するときに、それぞれ異なる URI テンプレートにバインドします。
図 4 BookmarkController クラス
[HandleError]
public class BookmarkController : Controller
{
// underlying model
BookmarksRepository bmRepository = new BookmarksRepository();
// query methods
public ActionResult BookmarkIndex() { ... }
public ActionResult BookmarksByUserIndex(string username) { ... }
public ActionResult BookmarksByTagIndex(string tag) { ... }
public ActionResult UserIndex() { ... }
public ActionResult TagIndex() { ... }
public ActionResult Details(int id) { ... }
// create boomark
[Authorize]
public ActionResult Create() { ... }
[Authorize]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection) { ... }
// update bookmark
[Authorize]
public ActionResult Edit(int id) { ... }
[Authorize]
[AcceptVerbs(HttpVerbs.Put | HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection)
// delete bookmark
[Authorize]
public ActionResult Delete(int id) { ... }
[Authorize]
[AcceptVerbs(HttpVerbs.Delete | HttpVerbs.Post)]
public ActionResult Delete(int id, FormCollection collection) { ... }
}
残りは、bookmark インスタンスを作成、編集、および削除するメソッドです。論理操作ごとに 2 つのメソッドが存在しています。1 つは入力フォームを取得するメソッドで、もう 1 つはフォーム送信に応答するメソッドです。どちらのメソッドにも承認が必要です。Authorize 属性によって、呼び出し元が認証され、コントローラー メソッドへのアクセスが承認されるようにします (この属性では、ユーザーと役割を指定することもできます)。未認証または未承認のユーザーが、"[Authorize]" と注釈を付けたコントローラー メソッドにアクセスを試みると、承認フィルターがユーザーを AccountController の Logon メソッドに自動的にリダイレクトします。これにより、コンシューマーに "Logon" ビューが表示されます。
AcceptVerbs 属性を使用して、特定のコントローラー メソッドで処理される HTTP 動詞を指定します (既定値は GET です)。1 つのメソッドは、HttpVerb 値を OR で連結することで、複数の動詞を処理できます。2 つ目の Edit メソッドを PUT と POST の両方にバインドしているのは、ブラウザーに適応するためです。このように構成することで、ブラウザーは POST を使用して操作を実行でき、ブラウザー以外のコンシューマーは PUT を使用できます (これはより適切です)。2 つ目の Delete メソッドでも同じように構成して、DELETE と POST の両方にバインドしています。ここで示したメソッドの実装では、2 度の要求が行われても 1 度しか処理されないように注意する必要があります。これは PUT と DELETE の両方の要件です。
これらのメソッドのいくつかが実装されるようすを見てみましょう。最初は BookmarkIndex です。
public ActionResult BookmarkIndex()
{
var bms = bmRepository.FindAllPublicBookmarks().ToList();
return View("BookmarkIndex", bms);
}
この実装では、Bookmark の公開エンティティの一覧をリポジトリから取得して、BookmarkIndex というビューを返します (Bookmark エンティティの一覧を渡します)。このビューは、コントローラーから提供される Bookmark エンティティの一覧を表示します。
Details メソッドは、対象のブックマークを探し、見つからない場合は 404, "Not Found" エラーを返します。次に、ユーザーを承認して、ブックマークを表示できるようにします。ユーザーが承認されたら、特定された Bookmark エンティティを提供する Details ビューを返します。これ以外の場合は、コンシューマーに Unauthorized 応答を返します。
public ActionResult Details(int id)
{
var bm = bmRepository.FindBookmarkById(id);
if (bm == null)
throw new HttpException(404, "Not Found");
if (!bm.Shared)
{
if (!bm.Username.Equals(HttpContext.User.Identity.Name))
return new HttpUnauthorizedResult();
}
return View("Details", bm);
}
最後の例として、2 つの Create メソッドを見てみましょう。1 つ目は、実際には非常に単純です。新しいブックマークを作成するフォーム記述を表示する Create ビューを返します。
[Authorize]
public ActionResult Create()
{
return View("Create");
}
図 5 に示す 2 つ目の Create メソッドは、フォームの送信要求に応答します。このメソッドでは、着信した FormCollection オブジェクトにあるブックマーク情報から新しい Bookmark エンティティを作成し、このエンティティをデータベースに保存します。次に、ブックマークに関連付けられていたすべての新しいタグでデータベースを更新してから、ユーザーを個人ブックマーク一覧にリダイレクトして、成功したことを示します。
図 5 フォームの送信要求に応答する Create メソッド
[Authorize]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection)
{
try
{
Bookmark bm = new Bookmark();
bm.Title = collection["bookmark-title"];
bm.Url = collection["bookmark-url"];
bm.Shared = collection["bookmark-shared"].Contains("true");
bm.LastModified = DateTime.Now;
bm.Username = HttpContext.User.Identity.Name;
bm.Tags = collection["bookmark-tags"];
bmRepository.AddBookmark(bm);
bmRepository.Save();
... // create any new tags that are necessary
return RedirectToAction("BookmaryByUserIndex",
new { username = HttpContext.User.Identity.Name });
}
catch
{
return View("Error");
}
}
コントローラーの実装全体を取り上げることはできませんが、これらのコード サンプルによって、コントローラーでどのような種類のコードを記述するかがわかります。
ルートを含む URI をデザインする
次に行うのは、さまざまな BookmarkController メソッドにマップする URL ルートの定義です。RegisterRoutes メソッド内で、Global.asax でアプリケーション ルートを定義します。最初に MVC プロジェクトを作成するとき、Global.asax には既定のルーティング コードを含みます (図 6 参照)。
図 6 既定のルーティング コード
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller="Home", // Parameter defaults
action="Index", id="" }
);
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
MapRoute の 1 回の呼び出しによって、既定のルーティング規則が作成されます。この規則はすべての URI に対応するように動作します。このルートは、最初のパス セグメントがコントローラー名、2 つ目がアクション名 (コントローラー メソッド)、3 つ目が ID 値を表します。この 1 つの規則によって、以下の URI を処理し、適切なコントローラー メソッドにルーティングできます。
/Account/Logon
/Bookmark/Create
/Bookmark/Details/25
/Bookmark/Edit/25
図 7 に、このブックマーク サービスの例で使用するルートを示します。URI に追加されたこれらのルートにより、コンシューマーは "/users" を参照してユーザー一覧を取得したり、"/tags"を参照してタグ一覧を取得したり、"/bookmarks" を参照して公開ブックマークの一覧を取得したりできます。コンシューマーは、"/tags/{タグ名}" または "/users/{ユーザー名}" を参照して、タグまたはユーザー名によってそれぞれブックマークにフィルターを適用することもできます。他のすべての URI は、既定のルートで処理されます (図 6 参照)。
図 7 ブックマーク サービスのルート
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// customer routes
routes.MapRoute("Users", "users",
new { controller = "Bookmark", action = "UserIndex" });
routes.MapRoute("Tags", "tags",
new { controller = "Bookmark", action = "TagIndex" });
routes.MapRoute("Bookmarks", "bookmarks",
new { controller = "Bookmark", action = "BookmarkIndex" });
routes.MapRoute("BookmarksByTag", "tags/{tag}",
new { controller = "Bookmark", action = "BookmarksByTagIndex", tag = "" });
routes.MapRoute("BookmarksByUser", "users/{username}",
new { controller="Bookmark", action="BookmarksByUserIndex", username="" });
// default route
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = ""});
}
ビューを実装する
ここまでは、実行したほとんどの処理が MVC の "サイト" にも "サービス" にも当てはまりました。すべての MVC アプリケーションでは、モデル、コントローラー、およびルートが必要です。MVC の "サービス" の構築に関して異なる点のほとんどは、ビューで生じます。サービスでは、人間が使用する従来の HTML ビューを作成するのではなく、人間とプログラムの両方が使用するのに適したビューを作成しなければなりません。
ここでは、既定のサービス表現に XHTML を使用し、前述の技法を適用してブックマーク データを XHTML にマップします。データ エンティティを <div> 要素と <span> 要素にマップし、<ol> と <li> の組み合わせを使用してコレクションを表します。また、これらの要素に "class" 属性で注釈を付けて、コンシューマーに追加の型メタデータを提供します。
ASP.NET MVC の "ビュー" は、ビューのテンプレートを定義する .aspx ページです。.aspx ページは、Views ディレクトリ内のコントローラー名で編成されます。各ビューを ASP.NET マスター ページと関連付けることで、一貫したテンプレートを保持できます。図 8 に、ブックマーク サービスのマスター ページを示します。
図 8 ブックマーク サービスのマスター ページ
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title><asp:ContentPlaceHolder ID="Title" runat="server" /></title>
</head>
<body>
<div style="text-align:right">
<% Html.RenderPartial("LogOnUserControl"); %>
</div>
<h1><asp:ContentPlaceHolder ID="Heading" runat="server" /></h1>
<hr />
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
<hr />
<div class="nav-links-footer">
<%=Html.ActionLink("Home", "Index", "Home", null,
new { @class = "root-link" } )%> |
<%=Html.ActionLink("Public Bookmarks", "BookmarkIndex", "Bookmark", null,
new { @class = "public-bookmarks-link" } )%> |
<%=Html.ActionLink("User Bookmarks", "BookmarksByUserIndex", "Bookmark",
new { username = HttpContext.Current.User.Identity.Name },
new { @class = "my-bookmarks-link" })%> |
<%=Html.ActionLink("Users", "UserIndex", "Bookmark", null,
new { @class = "users-link" } )%> |
<%=Html.ActionLink("Tags", "TagIndex", "Bookmark", null,
new { @class = "tags-link" } )%>
</div>
</body>
</html>
マスター ページは、ページ タイトル用、<h1> 見出し用、およびメイン コンテンツ領域用に 3 つのプレースホルダーを定義します。これらのプレースホルダーは、個々のビューから設定されます。また、マスター ページは、ページ上部にログイン コントロールを表示し、ルート サービスのリンクを含むフッターを提供して、ナビゲーションを簡素化します。ここでは Html.ActionLink メソッドを使用し、定義済みのルートとコントローラーのアクションに基づいて、これらのリンクを生成しています。
図 9 に、"/bookmarks" を参照すると返される、主要ブックマークの Index ビューを示します。このビューでは、<ol>、<li>、および <a> の各要素を組み合わせて、ブックマーク一覧を表示します。<ol> 要素には、class="bookmark-list" という注釈、各 <a> 要素には class="bookmark-link" という注釈を付けています。また、このビューでは、ブックマークの作成フォームの記述を取得するリンクを提供します (一覧の真上にあります)。リンクにナビゲーションを行うと、図 10 の Create ビューが機能します。
図 9 ブックマークの Index ビュー
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.
Master" Inherits="System.Web.Mvc.ViewPage
<IEnumerable<MvcBookmarkService.Models.Bookmark>>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="Title" runat="server">
Public Bookmarks</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="Heading" runat="server">
Public Bookmarks</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="MainContent" runat="server">
<%= Html.ActionLink("Create bookmark", "Create", "Bookmark", new { id = "" },
new { @class = "create-bookmark-form-link" } )%>
<ol class="bookmark-list">
<% foreach (var item in Model) { %>
<li><%= Html.ActionLink(Html.Encode(item.Title), "Details", "Bookmark",
new { id = item.BookmarkID }, new { @class = "bookmark-link" })%></li>
<% } %>
</ol>
</asp:Content>
図 10 ブックマークの Create ビュー
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<MvcBookmarkService.Models.Bookmark>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="Title" runat="server">
Create Bookmark</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="Heading" runat="server">
Create Bookmark</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm("Create", "Bookmark", FormMethod.Post,
new { @class = "create-bookmark-form" } ))
{%>
<p>
<label for="Title">Title:</label><br />
<%= Html.TextBox("bookmark-title")%>
</p>
<p>
<label for="Url">Url:</label><br />
<%= Html.TextBox("bookmark-url")%>
</p>
<p>
<label for="Tags">Tags:</label><br />
<%= Html.TextBox("bookmark-tags")%>
</p>
<p>
<label for="Shared">Share with public: </label>
<%= Html.CheckBox("bookmark-shared")%>
</p>
<p>
<input type="submit" value="Create" name="submit" />
</p>
<% } %>
</asp:Content>
Create ビューでは、単純な XHTML フォームを生成しますが、<form> 要素には class="create-bookmark-form" で注釈を付け、各 <input> 要素には、各ブックマーク フィールドを識別するコンテキストの名前/ID 値を指定します。このフォームでは、サービスを使用して新しいブックマークをプログラムから作成する方法に関する、完全な XHTML 記述が、(フォームを送信するだけで) コンシューマーに提供されます。
最後の例として、図 11 に、ブックマークの Details ビューの冒頭部分を示します。ここでは、<div> 要素を使用してブックマークの構造を表し (class="bookmark")、<span> 要素と <a> 要素を使用してブックマークのフィールドを表します。各要素に含まれる "class" 属性によって、フィールド名を指定します。
図 11 ブックマークの Details ビュー
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<MvcBookmarkService.Models.Bookmark>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="Title" runat="server">
Bookmark Details: <%= Model.BookmarkID %></asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="Heading" runat="server">
Bookmark Details: <%= Model.BookmarkID %></asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="MainContent" runat="server">
<br />
<div class="bookmark">
BookmarkID: <span class="bookmark-id"><%= Html.Encode(Model.BookmarkID) %></span><br />
Title: <span class="bookmark-title"><%= Html.Encode(Model.Title) %></span><br />
Url: <a class="bookmark-url-link" href="<%= Html.Encode(Model.Url) %>">
<%= Html.Encode(Model.Url) %></a><br />
Username: <%=Html.ActionLink(Model.Username, "BookmarksByUserIndex", "Bookmark",
new { username=Model.Username }, new { @class="bookmark-username-link" }) %><br />
...
ここではすべてのビューの例を詳しく説明することはできませんが、この例で、アプリケーションと人間の両方が使用しやすい、わかりやすい XHTML の結果セットを生成する方法をおわかりいただけたと思います。
ブックマーク サービスを使用する
ブックマーク サービスを使用する最も簡単な方法は、Web ブラウザーを使用することです。XHTML デザインのおかげで、サービスのルート URL を参照して、そこからナビゲーションを開始できます。図 12 に、ブックマーク サービスのルートを参照してログインするときのブラウザーの外観を示します。[Public Bookmarks] をクリックして、すべての公開ブックマーク一覧に移動してから、一覧にある個々のブックマークに移動できます。[Edit] をクリックすると、ブックマークの詳細を実際に編集できます (図 13 参照)。このサービスは、どんな Web ブラウザーからでも完全に使用できます。
図 12 MVC ブックマーク サービスの参照
図 13 特定のブックマークの編集
サービスを参照しているときに、ブラウザーでときどき [ソースの表示] をクリックすると、結果の XHTML の見た目が非常にシンプルなことに気が付くでしょう。このようなシンプルさが、プログラミングを容易にします。
図 14 に、ブックマーク サービスを使用する完全な .NET クライアント アプリケーションのコードを示します。このアプリケーションでは、前述した一連の XLinq 拡張メソッドを使用して、XHTML と HTTP の処理の詳細を簡素化します。このサンプルで興味深いのは、このサンプルがどちらかというと人間のように振る舞う点です。このサンプルでは、ブックマーク サービスで公開される他の項目にナビゲーションする場合、ルート URI のみが必要です。
図 14 XLinq を使用してブックマーク サービスを利用する .NET クライアントの作成
class Program
{
static void Main(string[] args)
{
// navigate to the root of the service
Console.WriteLine("Navigating to the root of the service...");
Uri uri = new Uri("http://localhost:63965/");
CookieContainer cookies = new CookieContainer();
var doc = XDocument.Load(uri.ToString());
doc.AddAnnotation(uri);
// navigate to public bookmarks
Console.WriteLine("Navigating to public bookmarks...");
var links = doc.Body().Ul("nav-links").Lis();
var bookmarksLink = links.Where(l => l.HasAnchor("public-bookmarks-link")).First();
var bookmarksDoc = bookmarksLink.Anchor().Navigate();
bookmarksDoc.AddAnnotation(cookies);
// display list of bookmarks
Console.WriteLine("\nPublic bookmarks found in the system:");
var bookmarks = bookmarksDoc.Body().Ol("bookmark-list").Lis();
bookmarks.Each(bm => Console.WriteLine("{0}: {1}",
bm.Anchor().AnchorText, bm.Anchor().AnchorLink));
// navigate to the first bookmark in the list
Console.WriteLine("\nNavigating to the first bookmark in the list...");
var bookmarkDoc = bookmarks.First().Anchor().Navigate();
var bookmarkDetails = bookmarkDoc.Body().Struct("bookmark");
// print the bookmark details out to the console window
Console.WriteLine("Bookmark details:");
Console.WriteLine("bookmark-id: {0}", bookmarkDetails["bookmark-id"].Value);
Console.WriteLine("bookmark-url-link: {0}",
bookmarkDetails["bookmark-url-link"].Value);
Console.WriteLine("bookmark-title: {0}", bookmarkDetails["bookmark-title"].Value);
Console.WriteLine("bookmark-shared: {0}", bookmarkDetails["bookmark-shared"].Value);
Console.WriteLine("bookmark-last-modified: {0}",
bookmarkDetails["bookmark-last-modified"].Value);
// retrieving login form
Console.WriteLine("\nRetrieving login form...");
Uri logonUri = new Uri("http://localhost:63965/Account/Logon");
var logonDoc = XDocument.Load(logonUri.ToString());
logonDoc.AddAnnotation(logonUri);
logonDoc.AddAnnotation(cookies);
// logging on as skonnard
Console.WriteLine("Logging in as 'skonnard'");
var logonForm = logonDoc.Body().Form("account-logon-form");
logonForm["username"] = "skonnard";
logonForm["password"] = "password";
logonForm.Submit();
Console.WriteLine("Login successful!");
// create a new bookmark as 'skonnard'
var createBookmarkDoc = bookmarksDoc.Body().Anchor(
"create-bookmark-form-link").Navigate();
createBookmarkDoc.AddAnnotation(cookies);
var createBookmarkForm = createBookmarkDoc.Body().Form("create-bookmark-form");
createBookmarkForm["bookmark-title"] = "Test from console!";
createBookmarkForm["bookmark-url"] = "https://live.com/";
createBookmarkForm["bookmark-tags"] = "Microsoft, Search";
createBookmarkForm.Submit();
Console.WriteLine("\nBookmark created!");
}
}
まず、クライアントはルート アドレスに移動して、公開ブックマークへのリンクを探します。次に、公開ブックマーク一覧に移動して、目的のブックマーク (この場合は、1 つ目のブックマーク) を特定します。その後、ブックマークの詳細に移動して、この情報をコンソール ウィンドウに表示します。すると、ログイン フォームが取得され、一連の資格情報を使用してログインが実行されます。ログインすると、アプリケーションがブックマーク作成フォームを取得するので、これに入力し、システムに新しいブックマークを送信します。
ここで、いくつか重要な点を説明しておきます。まず、コンソール アプリケーションでは、人間が Web ブラウザーを通じて実行できるすべての処理を実行できます。それこそが、この XHTML デザイン スタイルが優れている点です。次に、コンシューマーで必要なのは、サービスから公開されるルート URI をハードコーディングすることだけです。他のすべての URI は、XHTML 内で見つかったリンクに移動することで、実行時に検出可能です。最後に、XHTML 構造の処理は、他の構造の処理とそれほど変わりありません。どの構造も単なるデータにすぎません。そのうえ、こういった種類のコードは、.NET の今後のバージョンで動的言語に移行していくにつれて、さらに容易になっていきます。
最終的に、ASP.NET MVC では、人間とアプリケーションの両方が同時に使用できる、XHTML ベースのリソースの Web を実装するための、本質的に RESTful なフレームワークが提供されます。ここで示したサンプル アプリケーションの完全版は、MSDN マガジン Web サイトからダウンロードできます。
実際に見つかる XHTML ベースの RESTful サービスの、その他の完全な例については、Microsoft/TechNet Publishing System (MTPS) コンテンツ サービス (英語) を参照してください。このサービスでは、この記事で説明した多くの方法が使用されています。
謝辞
Tim Ewald と Craig Andera のお 2 人に感謝します。この分野に関する両名の創造的な考え方は、私の記事にとって良い刺激になりました。また、Tim は、付属のサンプル アプリケーションに含まれる XLinq 拡張メソッドも提供してくださいました。
Aaron Skonnard は、インストラクター主導とオンデマンドの両方の開発者向けコースを提供する、マイクロソフトのトレーニング プロバイダーである Pluralsight の共同設立者です。最近は、クラウド コンピューティング、Windows Azure、WCF、および REST を対象とする Pluralsight On-Demand! コースの録画にほとんどの時間を費やしています。連絡先は http://pluralsight.com/aaron (英語) です。同氏の近況については、http://twitter.com/skonnard (英語) にアクセスしてください。