次の方法で共有


すてきな ASP.NET

ASP.NET MVC Controller の動作

Scott Allen

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

この記事は、ASP.NET MVC Framework のプレリリース版に基づいて書かれています。記載されている情報は変更される可能性があります。

目次

すべてのルートはファクトリに通ず
ファクトリの拡張性
それは実行で始まる
セレクタ属性
フィルタ属性
カスタム アクション フィルタ
結果を取得する
アクション終了

コントローラは、Model View Controller (MVC) 設計パターンの根幹です。最前線に配置されて、最初にクライアントの要求を受け取り、次に、受け取った要求をモデル (アプリケーションのドメイン ロジックとデータが含まれている) に対する命令に変換します。また、ユーザーに情報を提示するためのビューを選択するのもコントローラの役割です。

この記事では、ASP.NET MVC フレームワークについて詳しく解説し、コントローラがどのように動作するのかを見ていきます。フレームワークとコントローラが対話するしくみ、およびユーザーがこの対話を操作する方法を説明します。コントローラ ファクトリ、コントローラ アクション、アクション フィルタ、およびアクションの結果についても説明します。

この記事ではかなり細部まで掘り下げて説明するので、ASP.NET MVC フレームワークのもっと一般的な概要を知りたい場合は、Chris Tavares の記事「Web フォームを使用しないで Web アプリケーションを作成する」を参照してください。

すべてのルートはファクトリに通ず

ルートのことに触れずにコントローラの生涯を語るのは容易ではありません。ASP.NET アプリケーションのルーティング テーブルには、ASP.NET のルーティング モジュールが受信 URL から情報を抽出して要求を適切なソフトウェア コンポーネントに送るために必要な情報が含まれます。1 月号のコラムでは、Web フォームでの ASP.NET ルーティング モジュールの使用について解説しました (「ASP.NET Web フォームによるルーティング」)。そのときのコラムでは Web フォームを実行するために独自のルーティング ハンドラを作成しましたが、ASP.NET MVC フレームワークには、最終的にいずれかのコントローラに要求を送るルーティング ハンドラが用意されています。

この MVC ルーティング ハンドラで要求を処理するには、アプリケーションの起動時にルーティング テーブルを構成する必要があります。MVC プロジェクト テンプレートで提供される既定のルーティング構成は global.asax ファイルに記述されており、その内容は図 1 のとおりです。

図 1 既定のルーティング構成

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        "Default",                                             
        "{controller}/{action}/{id}",                          
        new { controller = "Home", action = "Index", id = "" } 
    );            

}

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
}

図 1 のルーティング構成エントリの 1 つに Default という名前のルートがあり、"{controller}/{action}/{id}" という URL テンプレートが指定されています。ルーティング エンジンは、この URL テンプレートのパターンを使用して、受信 URL がこのルートと一致するかどうかを最初に確認します。そのようなルートと一致する URL として、http://localhost/home/index/ があります。ルーティング エンジンは、一致を検出すると再び URL テンプレートをパターンとして使用し、受信 URL からパラメータを取得します。この例では、{controller} の位置にある文字列 "home" がコントローラ パラメータになり、文字列 "index" がアクション パラメータになります。

MapRoute の 3 番目のパラメータとして指定される匿名型オブジェクトは、ルーティング エンジンが URL から特定のパラメータを検出できない場合に使用される既定値を表します。http://localhost/home/index/ の場合、ルーティング エンジンは URL から "id" の情報を検出しませんが、既定値である空の文字列を id パラメータで渡します。これらのすべてのパラメータは、ルーティング エンジンから RouteData オブジェクトを介してルーティング ハンドラに渡されます。

重要なこととして、ルーティング エンジンは ASP.NET MVC について何も知りません。ルーティング エンジンの唯一の仕事は、URL を分析してルート ハンドラに制御を渡すことです。図 1 の RegisterRoutes メソッドの内部で呼び出される MapRoute メソッドは、MVC フレームワークが提供する拡張メソッドです。MapRoute に登録されたすべてのルートは、MvcRouteHandler オブジェクトを使用するように構成されます。MvcRouteHandler オブジェクトは、MVC フレームワークが提供するルート ハンドラです。1 月号のコラムでご覧になったとおり、要求の HTTP ハンドラを見つけるのはルート ハンドラの仕事です。HTTP ハンドラは、IHttpHandler インターフェイスを実装するオブジェクトです。MVC アプリケーションでは、このオブジェクトは MvcHandler 型のオブジェクトであり、処理は MvcHandler の内部でおもしろくなってきます。

図 2 は、一般的な MVC 要求の処理フローを示しています。制御が MvcHandler に達した時点で、MvcHandler は、それまでの処理でルーティング モジュールによって生成された RouteData からコントローラ パラメータを抽出できます。ハンドラは最後に文字列値であるこのコントローラ パラメータをコントローラ ファクトリに送信します。その後にコントローラを作成して返すのはファクトリの役割です。MVC アプリケーション内のすべてのコントローラは IController インターフェイスを実装します。

fig02.gif

図 2 標準的な MVC 要求の制御フロー

MVC フレームワークには、その名も DefaultControllerFactory という既定のコントローラ ファクトリが用意されています。このファクトリは、アプリケーション ドメイン内のすべてのアセンブリから、IController を実装していて名前が "Controller" で終わるすべての型を検索します。つまり、このファクトリに対して "Home" コントローラの検索を指示した場合、このファクトリは、IController を実装してさえいれば属している名前空間やアセンブリには関係なく、新しくインスタンス化された HomeController クラスのインスタンスを返すことができます。この動作は、MVC の "設定より規約" スタイルの一部です。このファクトリについて説明することはまだありますが、MvcHandler の処理をまず終わらせましょう。

MvcHandler は、ファクトリから IController の参照を取得すると、コントローラに対する Execute を呼び出して、コントローラの処理が終わるのを待ちます。実行が完了すると、MvcHandler はコントローラが IDisposable インターフェイスを実装しているかどうかを確認し、実装している場合は、コントローラに対する Dispose を呼び出してアンマネージ リソースをクリーンアップします。

ファクトリの拡張性

コントローラ ファクトリは、ASP.NET MVC フレームワークにおける重要な拡張ポイントです。フレームワークで提供される既定のファクトリはソリューションのどこにある HomeController でも検出できますが、HomeController をインスタンス化できるのは、パラメータなしのコンストラクタを指定した場合だけです。この制限は、依存関係の逆転の原則に従い、コンストラクタを使用してコントローラの依存関係を注入しようとするチームにとっては問題です。例として、図 3 に示すような EmployeeController について考えます。このコントローラは、唯一のコンストラクタでログ コンポーネントを受け取る必要があります。

図 3 EmployeeController

public class EmployeeController : IController
{
    public EmployeeController(ILogger logger)
    {
        _logger = logger;
    }

    public void Execute(RequestContext requestContext)
    {
        // ...
    }

    ILogger _logger;
}

さいわいなことに、カスタム ファクトリを作成できます。IControllerFactory インターフェイスを実装するクラスはどれでもカスタム ファクトリの候補であり、必要なのは CreateController メソッドと ReleaseController メソッドを実装することだけです。そうではあるものの、StructureMap、Unity、Ninject、および Castle プロジェクトの Windsor などの制御の反転コンテナは簡単に使用でき、このシナリオに最適です。実際に、CodePlex にある MVC Contrib プロジェクトには、前に示したすべてのコンテナの IControllerFactory 実装が含まれています。

StructureMap を制御の反転コンテナとして使用する場合は、StructureMap および MvcContrib.StructureMap アセンブリを参照し、図 4 に示すようなコードを作成します。このリストの InitializeContainer メソッドは、最初に、ILogger が必要なときは常に SqlServerLogger 型を使用するよう StructureMap に指示します。その後、現在の ControllerBuilder の SetControllerFactory メソッドを使用して、アプリケーション全体のコントローラ ファクトリを設定します。要求処理の間に、MvcHandler は現在構成されているファクトリに対してこの同じ ControllerBuilder を要求し、既定のフレームワーク ファクトリの代わりに StructureMapControllerFactory を使用します。

図 4 InitializeContainer

protected void Application_Start()
{
    InitializeContainer();
    RegisterRoutes(RouteTable.Routes);
}

private void InitializeContainer()
{
    StructureMapConfiguration
        .ForRequestedType<ILogger>()
        .TheDefaultIsConcreteType<SqlServerLogger>();

    ControllerBuilder.Current.SetControllerFactory(
        new StructureMapControllerFactory());
}

MVC Contrib プロジェクトの StructureMapControllerFactory は、MVC フレームワークの既定のコントローラ ファクトリを継承し、インスタンス化するコントローラの型を特定するときは前に説明した方法を使用します。ただし、コントローラをインスタンス化するときは StructureMap を使用し、StructureMap はパラメータ化されたコンストラクタの処理方法を知っています。図 4 に示した構成が、http://localhost/Employee/ に対する要求の処理に必要なすべてになります。StructureMap は、SqlServerLogger の参照を渡すことにより、図 3 の EmployeeController をインスタンス化します。

fig05.gif

図 5 クラス階層

それは実行で始まる

ここまでに、MvcHandler はコントローラの Execute メソッドを呼び出して待機し、クリーンアップすると説明しました。これは、MvcHandler は IController インターフェイスを通してしかコントローラについて知ることができないためです。このレベルでアプリケーションを作成するのであれば、すべてのコントローラを IController インターフェイスから派生し、Execute メソッドを実装するだけで済みます。一方、ASP.NET MVC フレームワークでは、図 5 に示すようなクラスの階層を通して、さらに充実したコントローラの実行モデルが提供されています。

既定では、ASP.NET MVC プロジェクトに追加するコントローラは、System.Web.Mvc.Controller クラスから派生します。新しいコントローラを追加する 1 つの方法としては、ソリューション エクスプローラで [コントローラ] フォルダを右クリックし、[追加]、[コントローラ] の順にクリックします。図 6 のようなダイアログ ボックスが表示されます。コントローラがコントローラ ファクトリで検出されるためには、名前が "Controller" で終わっている必要があることを忘れないでください。

Controller 基本クラスでは、アクションの概念が導入されています。アクションは、MVC アプリケーションで要求の最終的な宛先となる、コントローラのメソッドです。これまでに、ASP.NET ルーティング モジュールは http://localhost/home/index/ の URL からコントローラ パラメータとして "home" を取得すると説明しました。これは、適切なコントローラに要求をルーティングするために十分な情報です。ルーティング モジュールは、アクション パラメータとして "index" も取得します。MvcHandler が HomeController に実行を指示すると、Controller 基本クラスに組み込まれているロジックが、このアクション パラメータを調べて、適切なコントローラ メソッドを呼び出します。このアクション ルーティング ロジックの大部分は、パブリック クラス ControllerActionInvoker の内部にあります。

fig06.gif

図 6 コントローラの追加

図 7 は、ASP.NET MVC プロジェクト テンプレートで提供されている HomeController です。パブリック インスタンス メソッドの Index と About は、それぞれ、クライアントが home/index/ および home/about/ を要求したときにフレームワークが呼び出すアクションを表します。呼び出す正しいアクションをフレームワークが確実に特定できる限り、任意のパブリック インスタンス メソッドをコントローラ アクションとして使用できます (言い換えると、メソッドのオーバーロードに注意する必要があるということです)。フレームワークが呼び出すアクションを検索するときに影響を及ぼすルールが他にもあります。フレームワークによるアクションの選択に影響を与えることや、アクションの選択に関する追加ルールを設定することができます。属性を使用してアクションの動作を操作することもできます。

図 7 HomeController

[HandleError]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        ViewData["Message"] = "Welcome to ASP.NET MVC!";

        return View();
    }

    public ActionResult About()
    {
        return View();
    }
}

セレクタ属性

セレクタ属性を使用してコントローラのアクションを修飾し、呼び出すアクションを選択するときに考慮する必要のある追加情報をフレームワークに提供できます。たとえば、[NonAction] 属性をコントローラ メソッドに追加すると、そのメソッドは使用可能なアクションの一覧から除外されます。メソッドに具体的なアクション名を設定することもできます。既定ではメソッド名が同時にアクション名になりますが、[ActionName("help")] を HomeController の About メソッドに設定すると、"about" はコントローラの有効なアクションではなくなります。About メソッドのアクション名は "help" になり、フレームワークは /home/help/ のような要求に対して About メソッドを呼び出します。

特に重要なセレクタ属性は、AcceptVerbs 属性です。この属性を指定すると、属性で列挙された動詞の中に現在の HTTP 要求の動詞と一致するものがあるときにのみ、フレームワークはアクションを選択できます。たとえば、メソッドを [AcceptVerbs(HttpVerbs.Post)] で修飾すると、そのメソッドは HTTP POST 操作のアクションとしてのみ呼び出すことができます。コントローラのアクションに対して適切な動詞を選択することが重要です。特に重要なのは、アクションがサーバーの状態を変更する場合です。詳細については、Stephen Walther の ASP.NET MVC Tip #46 (ASP.NET MVC ヒント #46)を参照してください。

フィルタ属性

アクション フィルタは、アクションに設定できる属性のもう 1 つの種類です。アクション フィルタでは、宣言型プログラミング モデルを使用して、アクションにキャッシュ、検証、およびエラー処理の動作を追加できます。図 7 の HomeController に対する [HandleError] 属性はフィルタ属性の例です。この属性は、個別のアクションに適用することも、コントローラ クラスに追加してコントローラの全アクションに動作を適用することもできます。

HandleError 属性がアクションに設定されている場合、アクションが例外をスローすると、MVC フレームワークは最初にコントローラのビュー フォルダで、次に共有ビュー フォルダで "Error" という名前のビューを検索します。エラー ビューを使用してユーザーにわかりやすいエラー ページを提供できます。また、より明示的な HandleError 属性を使用して例外を特定のビューにマップできます。たとえば、[HandleError(ExceptionType=typeof(SqlException), View="DatabaseError")] はハンドルされない SqlException を "DatabaseError" という名前のビューにマップします。MVC フレームワークで提供されている他のアクション フィルタの概要を図 8に示します。

図 8 アクション フィルタ
名前 説明
OutputCacheAttribute ASP.NET Web フォームの OutputCache ディレクティブと似ています。OutputCache 属性を指定すると、MVC フレームワークはコントローラの出力をキャッシュできます。
ValidateInputAttribute Web フォームの ValidateRequest 属性と似ています。既定では、MVC フレームワークは受信する HTTP 要求で HTML または他の危険な入力の有無を検査します。検出されると、例外が発生します。この属性を使用すると、要求の検証を無効にできます。
AuthorizeAttribute Authorize 属性を使用すると、宣言形式の承認チェックをコントローラ アクションに設定できます。この属性により、アクションを特定の役割のユーザーに制限できます。管理者の役割のユーザーだけが使用できるアクションを作成するときは、この属性を使用できます。
ValidateAntiForgeryTokenAttribute この属性は、クロスサイト リクエスト フォージェリ (CSRF) を回避するための解決策の半分を形成します。この属性を指定すると、フレームワークは HTTP POST 内にユーザー固有のトークンがあるかどうかを調べます。CSRF の詳細については、「Prevent Cross-Site REquest Forgery (CSFR) using ASP.NET MVC's AntiForgeryToken() helper (ASP.NET MVC の AntiForgeryToken() ヘルパを使用してクロスサイト リクエスト フォージェリ (CSFR) を防ぐ)」を参照してください。

カスタム アクション フィルタ

独自のアクション フィルタを作成し、アクションをカスタム ロジックで囲むことができます。図 9は、デバッグ中に Visual Studio の出力ウィンドウへの書き込みを行う簡単なログ記録アクション フィルタのコードです。この属性は、個別のアクションに適用することも、コントローラ クラスに設定してコントローラの全アクションのログを記録することもできます。

図 9 ログ記録アクション フィルタ

public class LogAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Log("Action Executing", filterContext.RouteData);
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        Log("Action Executed", filterContext.RouteData);
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        Log("Result Executing", filterContext.RouteData);
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        Log("Result Executed", filterContext.RouteData);            
    }

    void Log(string stageName, RouteData routeData)
    {
        Debug.WriteLine(
            String.Format("{0}::{1} - {2}", 
                routeData.Values["controller"],
                routeData.Values["action"],
                stageName));
    }        
}

見てのとおり、ActionFilter 基本クラスによって 4 つの仮想メソッドが提供されています。これらのメソッドをオーバーライドして、コントローラ アクションの前処理や後処理だけでなく、コントローラ アクションの結果の前処理や後処理も行うことができます。アクション実行前に OnActionExecuting メソッドが呼び出され、アクションが完了すると OnActionExecuted メソッドが呼び出されます (このメソッドは、アクションがハンドルされない例外をスローする場合でも呼び出されます)。同様に、OnResultExecuting メソッドが結果実行の前に呼び出され、OnResultExecuted メソッドが後で呼び出されます。

アクション フィルタ メソッドに渡されるコンテキスト パラメータを使用して、HTTP 要求、HTTP コンテキスト、ルート データなどを検査できます。これらのメソッドのいずれかから例外がスローされると、要求処理フローが停止します。環境内の前提条件を検査する ActionFilter を作成する場合、例外は便利なツールです。

結果を取得する

MVC コントローラ アクションの実行が成功すると、ActionResult から派生オブジェクトが生成されます。ビューのレンダリングと新しい URL へのブラウザのリダイレクトは、両方とも、コントローラから得ることができる有効な種類の結果です。ActionResult 派生型の詳細な一覧を図 10 に示します。

図 10 ActionResult の派生型
名前 フレームワークの動作 生成メソッド
ContentResult 文字列値を HTTP 応答に直接書き込みます。 Content
EmptyResult HTTP 応答に書き込みません。  
FileContentResult ファイルの内容 (バイトの配列として表される) を取得し、HTTP 応答に書き込みます。 File
FilePathResult 指定した場所にあるファイルの内容を取得し、HTTP 応答に書き込みます。 File
FileStreamResult コントローラによって生成されたファイル ストリームを取得し、HTTP 応答に書き込みます。 File
HttpUnauthorizedResult 承認チェックが失敗したときに承認フィルタによって使用される特殊な結果です。  
JavaScriptResult クライアントが実行するスクリプトをクライアントに返します。 JavaScript
JsonResult JavaScript Object Notation (JSON) のデータをクライアントに返します。 Json
RedirectResult クライアントを新しい URL にリダイレクトします。 Redirect
RedirectToRouteResult 指定されたビューをレンダリングして HTML フラグメントを返します (通常は AJAX のシナリオで使用されます)。 RedirectToRoute / RedirectToAction
PartialViewResult 指定されたビューをレンダリングして HTML フラグメントを返します (通常は AJAX のシナリオで使用されます)。 PartialView
ViewResult 指定されたビューをレンダリングして HTML をクライアントに返します。 View

コントローラ アクションはこれらのどの型も直接インスタンス化する必要がないことに注意してください。その代わりに、コントローラ アクションは図 10 に示したメソッド名を呼び出して結果を生成できます。これらのメソッドは MVC コントローラの基本クラスから継承されます。また、コントローラ アクションは ActionResult オブジェクトを返す必要がないことも重要です。コントローラから ActionResult 以外のオブジェクトが返された場合、フレームワークはそのオブジェクトを文字列に変換して ContentResult (単純に文字列を HTTP 応答に書き込む) 内にラップします。void を返すコントローラは EmptyResult を生成します。

ActionResult クラスには ExecuteResult メソッドが定義されています。このメソッドは、図 10 の各型によってオーバーライドされます。このメソッドは ControllerActionInvoker (コントローラのアクションを呼び出したオブジェクトと同じ) によって呼び出されます。呼び出された結果は、それぞれ、結果をクライアントに正しく提供するために必要なすべての細部を処理します。たとえば、JavaScript の結果は応答のコンテンツ タイプ ヘッダーを "application/x-javascript" に設定し、HttpUnauthorizedResult は応答の HTTP ステータス コードを 401 (未承認) に設定します。

コントローラ アクションからの一般的な応答は ViewResult です。前のコード リストでは、すべてのアクションがコントローラの View メソッドを呼び出して結果を返しているので、ViewResult で結果が表示されます。これは "設定より規約" のもう 1 つの例です。この既定の方法で ViewResult を使用すると、ViewResult はコントローラのビュー フォルダでアクションに一致するファイル名を持つビューを検索します。たとえば、views\home\about.aspx は Home コントローラの About アクションの規約上のビューです。View メソッドのオーバーロード バージョンを使用することで、ビューの名前を明示的に指定できます。

アクション終了

今月は、ASP.NET MVC コントローラにまつわる抽象化と動作について説明しました。MVC フレームワークでのコントローラの検出、作成、および使用方法と、コントローラに関係した MVC フレームワークの拡張ポイントでフックを作成する方法がよくおわかりいただけたことと思います。次回は、コントローラを実際のアプリケーションで動作させるためのガイドラインとベスト プラクティスについて説明します。

インサイト: ヘルパ メソッド

View() や Content() などのヘルパ メソッドがこのフレームワークに初めて登場したときに、アクションの結果に対してヘルパ メソッドを使用することに違和感を感じた読者は、ヘルパ メソッドが登場した経緯や、その特定の設計上の決定がなされた背景が何であったかを知りたいと思ったかもしれません。

アクションの結果を返すためのヘルパ メソッドの背景には、MVC 開発者の MVC アプリケーション作成時間の 99% がコントローラ アクションの作成に費やされるということがあります。マイクロソフトでは、一般的なメソッドを簡潔でわかりやすく、できる限り宣言型にしたいと考えました。

たとえば、次のようなアクション メソッドを作成することは依然として可能です。

public ActionResult Foo {
    // ... do stuff ...
    ViewData.Model = myModel;
    return new ViewResult {ViewName = "Foo", ViewData = this.ViewData};
}

これをもう少し簡潔にしたかったので、若干の手直しを加えて次のようにしました。

public ActionResult Foo {
    // ... do stuff ...
    return new View(myModel);
}

これは非常に命令型の言語を使用しているにもかかわらず、より宣言型の方法です。アクション メソッドを見ると、作成者の意図が反映されています。それは、"このモデルを含むビューを返したい" ということです。

-- Phil Haak、マイクロソフト、シニア プログラム マネージャ

ご意見やご質問は xtrmasp@microsoft.com まで英語でお送りください。

K. Scott Allen は Pluralsight 技術スタッフのメンバであり、OdeToCode の創設者です。彼の連絡先は scott@OdeToCode.com、ブログのアドレスは odetocode.com/blogs/scott です。