すてきな ASP.NET
ASP.NET Web フォームによるルーティング
Scott Allen
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照
目次
ルーティングとは
URL リライトの歴史概略
ルートとルート ハンドラ
ASP.NET をルーティング用に構成する
ルートを構成する
レシピのルーティング用ハンドラ
ルーティングとセキュリティ
URL の生成
ルートのまとめ
Microsoft .NET Framework 3.5 Service Pack 1 では、ASP.NET ランタイムにルーティング エンジンが導入されました。ルーティング エンジンでは、受信 HTTP 要求の URL を要求に応答する物理的な Web フォームから切り離すことができます。これにより、Web アプリケーションのフレンドリ URL を構築できます。フレンドリ URL は以前のバージョンの ASP.NET でも使用できましたが、ルーティング エンジンが導入されたことにより、さらに簡単で、明快で、テストしやすい手法が提供されるようになりました。
ルーティング エンジンは当初は、この記事の執筆時点ではプレビュー段階にある ASP.NET Model View Controller (MVC) フレームワークの一部でした。しかし、マイクロソフトはルーティング ロジックを System.Web.Routing アセンブリにパッケージ化し、そのアセンブリを SP1 でリリースしました。アセンブリは現在は ASP.NET Dynamic Data 機能 (SP1 でリリースされた) を使用して Web サイトのルーティングを提供していますが、このコラムでは、ASP.NET Web フォームでルーティング機能を使用する方法について説明します。
ルーティングとは
RecipeDisplay.aspx という名前の ASP.NET Web フォームがあるとします。このフォームが Web Forms というフォルダ内に格納されていると仮定します。Web フォームでレシピを表示するための従来の手法では、フォームの物理的な場所を指す URL を構築し、データをクエリ文字列にエンコードして、表示するレシピを Web フォームに伝えるという手順を実行します。そのような URL の末尾は、/WebForms/RecipeDisplay.aspx?id=5 のようになります。ここで、数字の 5 はレシピが含まれているデータベース テーブル内の主キーの値を表しています。
ルーティングとは基本的には、URL エンドポイントをパラメータに分解し、そのパラメータを使用して HTTP 要求の処理を特定のコンポーネントに導くことです。例として URL の /recipe/5 を取り上げましょう。ルーティングを適切に構成することにより、Web フォームの RecipeDisplay.aspx でこの URL に応答できます。
URL は物理的なパスを表すものではなくなります。代わりに、recipe という語により、ルーティング エンジンがレシピ要求を処理するためのコンポーネントを見つけるときに使用するパラメータを表します。5 という数値は、処理中に特定のレシピを表示するために必要な 2 番目のパラメータを表します。データベース キーを URL にエンコードする代わりに、/recipe/tacos のような URL を使用します。この URL は特定のレシピを表すのに十分なパラメータを含んでいるだけでなく、人間が読むことのできるものであり、エンド ユーザーに目的を示すことができ、検索エンジンで使用される重要なキーワードも含んでいます。
URL リライトの歴史概略
従来の ASP.NET では、/recipe/tacos で終わる URL を使用する場合は、URL リライト スキームが必要でした。URL リライトの詳細な情報については、Scott Mitchell による信頼性の高い記事「ASP.NET での URL 書き換え」を参照してください。この記事では、HTTP モジュールおよび HttpContext クラスの静的な RewritePath メソッドを使用して行う、ASP.NET での URL リライトの一般的な実装について説明されています。Scott の記事は、リライトしやすいフレンドリ URL の利点についても詳細に解説しています。
これまでに RewritePath API を使用したことのある皆さんは、リライト手法の特性や弱点についていくつかご存知でしょう。RewritePath の主要な問題は、要求の処理中にメソッドで使用される仮想パスをどのように変更するかということです。URL リライトでは、内部の再リライトされた URL へのポストバックを回避するために、各 Web フォームのポストバック先を修正する必要がありました (通常は、要求の間の 2 回目の URL リライトで)。
また、URL リライト ロジックを双方向で動作させるための簡単なメカニズムがなかったため、ほとんどの開発者は単一方向の変換として URL リライトを実装していました。たとえば、URL リライト ロジックに公開 URL を提供し、ロジックが Web フォームの内部 URL を返すようにすることは簡単でした。困難なのは、リライト ロジックに Web フォームの内部 URL を提供して、フォームに到達するために必要な公開 URL が返されるようにすることでした。後者は、リライト済み URL の背後に隠れている、他の Web フォームへのハイパーリンクを生成する場合に役立ちます。このコラムの後半で説明しますが、URL ルーティング エンジンはこれらの問題を解決できます。
図 1 ルート、ルート ハンドラ、およびルーティング モジュール
ルートとルート ハンドラ
URL ルーティング エンジンには 3 つの基本的な要素があります。それは、ルート、ルート ハンドラ、およびルーティング モジュールです。ルートは URL をルート ハンドラに関連付けます。System.Web.Routing 名前空間からの Route クラスのインスタンスは、実行時にルートを表し、ルートのパラメータおよび制約を示します。ルート ハンドラは、System.Web.Routing.IRouteHandler インターフェイスからの継承です。このインターフェイスは、IHttpHandler インターフェイスを実装するオブジェクトを返す GetHttpHandler メソッドを実装するようにルート ハンドラに要求します。IHttpHandler インターフェイスは、最初から ASP.NET の一部でした。また、Web フォーム (System.Web.UI.Page) は IHttpHandler です。Web フォームでルーティングを使用する場合、ルート ハンドラは適切な Web フォームを見つけ、インスタンス化して、返す必要があります。最後に、ルーティング モジュールは ASP.NET 処理パイプラインに組み込まれます。モジュールは受信要求をインターセプトし、URL を調べて、一致するルートが定義されているかどうかを検出します。モジュールでは、一致するルートに対して関連付けられたルート ハンドラを取得し、受信要求を処理する IHttpHandler をそのルーティング用ハンドラに要求します。
上述の 3 種類の要素を図 1 に示しています。次のセクションでは、この 3 つの要素の動作について説明します。
ASP.NET をルーティング用に構成する
ASP.NET Web サイトまたは Web アプリケーションをルーティング用に構成するには、最初に、System.Web.Routing アセンブリへの参照を追加する必要があります。.NET Framework 3.5 SP1 のインストールでは、このアセンブリがグローバル アセンブリ キャッシュにインストールされます。アセンブリは、標準の [参照の追加] ダイアログ ボックスにあります。
ルーティング モジュールを ASP.NET 処理パイプラインに構成することも必要です。ルーティング モジュールは標準 HTTP モジュールです。IIS 6.0 以前および Visual Studio Web 開発サーバーの場合は、次に示すように、web.config の <httpModules> セクションを使用してモジュールをインストールします。
<httpModules>
<add name="RoutingModule"
type="System.Web.Routing.UrlRoutingModule,
System.Web.Routing,
Version=3.5.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35"/>
<!-- ... -->
</httpModules>
IIS 7.0 でのルーティングで Web サイトを実行するには、web.config に 2 つのエントリが必要です。最初のエントリは、<system.webServer> の <modules> セクション内の URL ルーティング モジュール構成です。また、<system.webServer> の <handlers> セクションには、UrlRouting.axd に対する要求を処理するためのエントリが必要です。この両方のエントリを図 2 に示します。「IIS 7.0 構成エントリ」も参照してください。
図 2 URL ルーティング モジュールの構成
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="UrlRoutingModule"
type="System.Web.Routing.UrlRoutingModule,
System.Web.Routing, Version=3.5.0.0,
Culture=neutral,
PublicKeyToken=31BF3856AD364E35" />
<!-- ... -->
</modules>
<handlers>
<add name="UrlRoutingHandler"
preCondition="integratedMode"
verb="*" path="UrlRouting.axd"
type="System.Web.HttpForbiddenHandler,
System.Web, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a" />
<!-- ... -->
</handlers>
</system.webServer>
URL ルーティング モジュールをパイプラインに構成すると、モジュール自身が PostResolveRequestCache イベントおよび PostMapRequestHandler イベントに接続します。図 3 にパイプライン イベントのサブセットを示します。通常、URL リライトの実装が作業を実行するのは、要求の最も早い段階で発生する BeginRequest イベントが動作するときです。URL ルーティングでは、認証、承認、およびキャッシュ ルックアップという処理のステージの後に発生する PostResolveRequestCache ステージで、ルート マッチングとルートの選択が実行されます。このイベントのタイミングについては、コラムの後半で説明します。
図 3 HTTP 要求
ルートを構成する
ルートおよびルート ハンドラは連携して動作しますが、ここでは最初にルートを構成するコードについて説明します。ルーティング エンジンの RouteTable クラスは、静的な Routes プロパティを通して RouteCollection を公開します。アプリケーションで最初の要求の実行を開始する前に、すべてのカスタム ルートをこのコレクションに構成する必要があります。つまり、global.asax ファイルおよび Application_Start イベントを使用する必要があるということです。
図 4 に、"/recipe/brownies" で Web フォームの RecipeDisplay.aspx に到達するために必要なルート登録のコードを示します。RouteCollection クラスの Add メソッドのパラメータには、ルートのわかりやすい名前がルートの前に配置されています。Route コンストラクタの最初のパラメータは URL パターンです。URL パターンは、このアプリケーションを指す URL の末尾に含まれている URL セグメントで構成されています (アプリケーションのルートに到達するために必要なすべてのセグメントの後に配置されます)。アプリケーションのルートが localhost/food/ で示される場合、図 4 のルート パターンは localhost/food/recipe/brownies に一致します。
図 4 /recipe/brownies に対するルート登録のコード
protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes();
}
private static void RegisterRoutes()
{
RouteTable.Routes.Add(
"Recipe",
new Route("recipe/{name}",
new RecipeRouteHandler(
"~/WebForms/RecipeDisplay.aspx")));
}
IIS 7.0 構成エントリ
この例で私が行ったように拡張子のない URL を使用する場合は、runAllManagedModulesForAllRequests 属性の値を true にする必要があります。UrlRouting.axd に対して HTTP ハンドラを構成するのは奇妙に感じられるかもしれません。これは、IIS 7.0 でルーティングを実行するためにルーティング エンジンが必要とする簡単な対処法です。UrlRouting モジュールは実際は受信 URL を ~/UrlRouting.axd にリライトします。これにより、URL が元の受信 URL にリライトされます。IIS 将来のバージョンでルーティング エンジンと完全に統合された場合には、この対処法は不要です。
中かっこに囲まれたセグメントはパラメータを示しており、ルーティング エンジンはこのパラメータから自動的に値を抽出し、要求が継続している間は保持される name/value 辞書に配置します。前の例の localhost/food/recipe/brownies の場合、ルーティング エンジンは "brownies" という値を抽出し、その値を "name" というキーと共に辞書に格納します。辞書の使用方法については、ルート ハンドラのコードの説明の中で解説します。
RouteTable には必要な数だけルートを追加できますが、ルートの順序が重要なので注意してください。ルーティング エンジンでは、すべての受信 URL が、コレクション内のルートに対してルートが表示されている順序でテストされ、一致するパターンを持つ最初のルートが選択されます。したがって、最も具体的なルートを最初に追加する必要があります。レシピ ルートの前に "{category}/{subcategory}" という URL パターンを汎用ルートと共に追加した場合、ルーティング エンジンはレシピ ルートを見つけることができません。さらに注意すべき点がもう 1 つあります。ルーティング エンジンは大文字小文字を区別してパターン マッチングを実行します。
オーバーロードされたバージョンの Route コンストラクタにより、既定パラメータ値を作成し、制約を適用することができます。既定により、受信 URL の name/value パラメータ辞書に値がない場合には、ルーティング エンジンの既定値が配置されるように指定できます。たとえば、ルーティング エンジンが localhost/food/recipe のような name 値のないレシピ URL を見つけたときに "brownies" を既定のレシピ名にすることができます。
制約を使用して、正規表現でパラメータを検証し、受信 URL のルート パターン マッチングを微調整するように指定できます。localhost/food/recipe/5 のように主キー値を使用して URL 内のレシピを識別する場合、正規表現を使用することにより、URL 内の主キー値が整数であることを確認できます。また、IRouteConstraint インターフェイスを実装するオブジェクトを使用して制約を適用することもできます。
Route コンストラクタに対する 2 番目のパラメータは、ルート ハンドラの新しいインスタンスです (図 5 を参照)。
図 5 RecipeRouteHandler
public class RecipeRouteHandler : IRouteHandler
{
public RecipeRouteHandler(string virtualPath)
{
_virtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var display = BuildManager.CreateInstanceFromVirtualPath(
_virtualPath, typeof(Page)) as IRecipeDisplay;
display.RecipeName = requestContext.RouteData.Values["name"] as string;
return display;
}
string _virtualPath;
}
レシピのルーティング用ハンドラ
次のコード スニペットは、レシピの要求に対するルート ハンドラの基本的な実装を示しています。ルート ハンドラは最終的に IHttpHandler のインスタンス (この場合は RecipeDisplay.aspx) を作成する必要があるため、コンストラクタにはルート ハンドラにより作成される Web フォームを指す仮想パスが必要です。GetHttpHandler メソッドは、インスタンス化された Web フォームを取得するためにこの仮想パスを ASP.NET BuildManager に渡します。
interface IRecipeDisplay : IHttpHandler
{
string RecipeName { get; set; }
}
ルート ハンドラがどのようにしてルーティング エンジンのパラメータ辞書 (RequestContext クラスの RouteData プロパティ) からデータを取得することもできるかについて注意してください。ルーティング エンジンは、RequestContext を設定し、このメソッドを呼び出すときにインスタンスを渡します。ルート データを Web フォームに取得するための多数のオプションがあります。たとえば、HttpContext Items コレクションにルート データを渡すことができます。この例では、Web フォームに実装するためのインターフェイスを定義しました (IRecipeDisplay)。ルート ハンドラは、Web フォームに厳密に型指定されたプロパティを設定し、Web フォームに必要な情報を渡すことができます。この手法は、ASP.NET Web サイトと ASP.NET アプリケーションの両方のコンパイル モデルで有効です。
ルーティングとセキュリティ
ASP.NET ルーティングを使用する場合、使い慣れた ASP.NET 機能 (マスタ ページ、出力キャッシュ、テーマ、ユーザー コントロールなど) をすべて使用できます。ただし、注意すべき例外が 1 つあります。ルーティング モジュールの機能は、認証および承認という処理のステージの後で発生するパイプライン内のイベントを使用して動作します。つまり、ASP.NET がユーザーを承認する場合は、表示されるパブリック URL が使用され、ルート ハンドラが要求の処理のために選択する ASP.NET Web フォームへの仮想パスは使用されないということです。ルーティングを使用するアプリケーションの承認の戦略には、注意を払う必要があります。
たとえば、認証済みユーザーのみにレシピを表示する必要があるとします。1 つの手段は、次に示すように、ルートの web.config を変更して承認設定が使用されるようにする方法です。
<location path="recipe">
<system.web>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
</location>
この方法では、匿名ユーザーが /recipe/tacos を表示するのを防ぐことはできますが、2 つの基本的な弱点があります。最初に、この設定ではユーザーが /WebForms/RecipeDisplay.aspx を直接要求することは防止できません (ただし、すべてのユーザーが Web Forms フォルダからリソースを直接要求できないようにする別の承認規則を追加することはできます)。2 番目の弱点は、承認規則を変更せずに global.asax.cs でルート構成を簡単に変更できることです。これにより、匿名ユーザーが秘密のレシピを見ることができます。
承認の別の手段は、RecipeDisplay.aspx Web フォームを物理的な場所に基づいて保護する方法です。<authorization> 設定を含む web.config ファイルを保護されたフォルダに直接配置します。ただし、ASP.NET はパブリック URL に基づいてユーザーを承認するため、ルート ハンドラで使用される仮想パスで承認の確認を手動で行う必要があります。
次のコードをルート ハンドラの GetHttpHandler メソッドの最初に追加する必要があります。このコードでは、UrlAuthorizationModule クラスの静的な CheckUrlAccessForPrincipal メソッド (ASP.NET パイプラインで承認の確認を実行するモジュールと同じ) を使用します。
if (!UrlAuthorizationModule.CheckUrlAccessForPrincipal(
_virtualPath, requestContext.HttpContext.User,
requestContext.HttpContext.Request.HttpMethod))
{
requestContext.HttpContext.Response.StatusCode =
(int)HttpStatusCode.Unauthorized;
requestContext.HttpContext.Response.End();
}
RequestContext 経由で HttpContext メンバにアクセスするために、System.Web.Abstractions アセンブリへの参照を追加する必要があります。
安全なルーティング用ハンドラが配置されると、データベースの各レシピへのハイパーリンクを生成する必要のあるページに注意を向けることができるようになります。結果的に、ルーティング ロジックはこのページの構築にも役立つわけです。
URL の生成
特定のレシピへのハイパーリンクを生成するために、アプリケーション起動時に構成されたルートのコレクションにもう一度目を向けましょう。次に示すように、RouteCollection クラスはこの目的のために GetVirtualPath メソッドを保持しています。
VirtualPathData pathData =
RouteTable.Routes.GetVirtualPath(
null,
"Recipe",
new RouteValueDictionary { { "Name", recipeName } });
return pathData.VirtualPath;
目的のルート名 ("Recipe") を必要なパラメータの辞書および関連付けられた値と共に渡す必要があります。このメソッドでは、作成済みの URL パターン (/recipe/{name}) を使用して適切な URL が構築されます。
次のコードでは、このメソッドを使用して、匿名型オブジェクトのコレクションを生成します。このオブジェクトには、データ バインディングに使用して利用可能なレシピのリストまたはテーブルを生成できる、Name プロパティおよび Url プロパティが保持されています。
var recipes =
new RecipeRepository()
.GetAllRecipeNames()
.OrderBy(recipeName => recipeName)
.Select(recipeName =>
new
{
Name = recipeName,
Url = GetVirtualPathForRecipe(recipeName)
});
ルーティング構成から URL を生成できるということは、アプリケーション内のリンクを壊すことなく、構成を変更できるということです。もちろん、ユーザーのお気に入りリンクやブックマークを壊す可能性はありますが、アプリケーションの URL 構造を設計しているときにはこの変更機能が非常に役立ちます。
ルートのまとめ
URL ルーティング エンジンは、URL パターン マッチングと URL 生成に関する煩雑な作業をすべて引き受けてくれます。自分で行うことは、ルートを構成してルート ハンドラを実装する作業のみです。ルーティングを使用することで、ファイル拡張子とファイル システムの物理的なレイアウトから真に解放され、URL リライタを使用するうえでの特性に対処する必要がなくなります。代わりに、エンド ユーザーまたは検索エンジンにとって最適な URL デザインに専念できます。マイクロソフトでは、次の ASP.NET 4.0 で Web フォームでの URL ルーティングをより簡単にし、さらに構成可能にすることができるように作業を進めています。
ご意見やご質問は xtrmasp@microsoft.com まで英語でお送りください。
Scott Allen は OdeToCode の創設者であり、Pluralsight 技術スタッフのメンバです。彼の連絡先は scott@odetocode.com、ブログのアドレスは OdeToCode.com/blogs/scott です。