Cutting Edge

ASP.NET MVC の動的なアクション フィルター

Dino Esposito

Dino Esposito先月は、ASP.NET MVC アプリケーションにおけるアクション フィルターの役割と実装について説明しました。先月の内容を簡単におさらいしましょう。アクション フィルターは、コントローラー メソッドやコントローラー クラスを修飾する属性で、オプションのアクションを実行できるようにすることを目的としています。たとえば、Compress 属性を記述し、この属性で圧縮された gzip ストリームによってメソッドで生成された応答を透過的にフィルター処理できます。最大のメリットは、圧縮処理のコードを簡単に再利用できる個別のクラスに分離された状態で維持できることです。これにより、メソッドで対応しなければならない範囲をできる限り抑えられます。

ですが、属性は静的なものです。属性に備わっている柔軟性を享受するには、追加のコンパイル手順を実行する必要があります。コントローラー クラスの側面は簡単に変更できますが、ソース コードの変更という代償が伴います。ですが、一般的に、ソース コードの変更は、それほど大きな代償ではありません。多くのコードのメンテナンス作業では、ソース コードを物理的に変更する必要があります。このような変更を効率的かつ回帰テストが必要にならない方法で行えるのが最適です。

コンテンツと機能が頻繁に変化する Web サイト (多くの場合は Web ポータル) や細かくカスタマイズできるサービスとしてのソフトウェア (SaaS) アプリケーションでは、ソース コードを変更しないで済ませられるソリューションがあると便利です。そこで「アクション フィルターを動的に読み込むためにできることはあるのか」ということが問題になります。この記事の残りの部分では、アクション フィルターを動的に読み込む方法を紹介します。

ASP.NET MVC の内部

ASP.NET MVC フレームワークでは、全面的にカスタマイズできると言っても過言ではない多数のインターフェイスやオーバーライド可能なメソッドが公開されています。手短に説明すると、コントローラー メソッドの一連のアクション フィルターは、メモリ内のリストに読み込まれて保持されます。開発者は、このリストにアクセスして調査できます。追加の処理を行うと、アクション フィルターのリストを変更することが可能になり、動的にフィルターを設定することもできます。

MVC フレームワークがアクションを実行するために行う手順の概要を説明しながら、このしくみを詳しく見てみましょう。また、動的なフィルターを実現する主要コンポーネントであるアクションの呼び出し元についても説明します。

アクションの呼び出し元では、最終的にコントローラー クラスのアクション メソッドも実行します。アクションの呼び出し元では、ASP.NET MVC の各要求ごとに内部ライフサイクルを実装しています。呼び出し元は、IActionInvoker インターフェイスを実装するクラスのインスタンスです。各コントローラー クラスには、単純な get/set プロパティを通じて公開される ActionInvoker という名前の独自の呼び出し元オブジェクトがあります。このプロパティは、System.Web.Mvc.Controller の基本型として次のように定義されています。

public IActionInvoker ActionInvoker {

  get {

    if (this._actionInvoker == null) {

      this._actionInvoker = this.CreateActionInvoker();

    }

    return this._actionInvoker;

  }

  set {

    this._actionInvoker = value;

  }

}

CreateActionInvoker メソッドは、Controller 型の保護されたオーバーライド可能なメソッドです。このメソッドの実装を次に示します。

protected virtual IActionInvoker CreateActionInvoker() {

  // Creates an instance of the built-in invoker

  return new ControllerActionInvoker();

}

アクションの呼び出し元は、任意のコントローラーに合わせて自由に変更できます。ただし、呼び出し元は、要求のライフサイクルの早い段階の処理に関与しているので、既定の呼び出し元を独自の呼び出し元に変更するコントローラー ファクトリが必要になります。
Unity などの制御の反転 (IoC: Inversion of Control) フレームワークと組み合わせることで、このアプローチでは、呼び出し元のロジックを IoC コンテナーの (オフラインの) 設定から直接変更できるようになります。

また代替案として、アプリケーション用にカスタム コントローラー基本クラスを定義し、CreateActionInvoker メソッドをオーバーライドして、必要な呼び出し元オブジェクトのみを返すようにすることも可能です。これは、ASP.NET MVC フレームワークがコントローラーのアクションの非同期実行をサポートするために採用しているアプローチです。

アクションの呼び出し元は、IActionInvoker インターフェイスをベースに構築されており、1 つのメソッドしか公開していないので非常に単純です。

public interface IActionInvoker {

  bool InvokeAction(

    ControllerContext controllerContext, 

    String actionName);

}

既定のアクションの呼び出し元に注意しながら、アクションの呼び出し元で対応している主なタスクを確認しましょう。呼び出し元では、まず、要求の背後にあるコントローラーと実行するアクションに関する情報を取得します。情報はアドホックな記述子オブジェクトの形で提供されます。この記述子には、コントローラーの名前と種類に加えて、属性とアクションの一覧が含まれています。パフォーマンス上の理由から、呼び出し元では、アクションとコントローラーの記述子のキャッシュを独自に構築します。

図 1 に ControllerDescriptor クラスのプロトタイプを示します。これは、記述子の基本クラスの代表例です。

図 1 ControllerDescriptor クラス

public abstract class ControllerDescriptor : 

  ICustomAttributeProvider {



  // Properties

  public virtual string ControllerName { get; }

  public abstract Type ControllerType { get; }



  // Method

  public abstract ActionDescriptor[] GetCanonicalActions();

  public virtual object[] GetCustomAttributes(bool inherit);

  public abstract ActionDescriptor FindAction(

    ControllerContext controllerContext, 

    string actionName);

  public virtual object[] GetCustomAttributes(

    Type attributeType, bool inherit);

  public virtual bool IsDefined(

    Type attributeType, bool inherit);

}

ASP.NET MVC フレームワークでは、Microsoft .NET Framework のリフレクションを内部で多用する 2 つの記述子の具象クラスを採用しています。1 つは、ReflectedControllerDescriptor と言う名前のクラスです。もう 1 つは、非同期のコントローラーにのみ使用される ReflectedAsyncControllerDescriptor という名前のクラスです。

独自の記述子を作成しなければならない現実的なシナリオは思い付きませんが、興味がある方のために、その方法を紹介しましょう。

派生型の記述子クラスを作成し、GetCanonicalActions メソッドをオーバーライドして、構成ファイルまたはデータベース テーブルからサポートされているアクションの一覧を読み取るとします。このようにすることで、構成可能な内容に基づいて一覧から有効なアクション メソッドを削除できます。ただし、これが機能するためには、独自のアクションの呼び出し元を使用して、カスタム記述子のインスタンスを返すように呼び出し元用の GetControllerDescriptor メソッドを記述する必要があります。

protected virtual ControllerDescriptor 

  GetControllerDescriptor(

  ControllerContext controllerContext);

コントローラーとアクション メソッドに関する情報を取得することは、アクションの呼び出し元によって行われる一連の手順の始まりに過ぎません。次に、アクションの呼び出し元では、処理しているメソッドのアクション フィルターの一覧を取得します (これは、この記事のおもしろい部分です)。また、アクションの呼び出し元では、ユーザーの認証とアクセス許可をチェックし、潜在的な危険がある投稿されたデータに対する要求を検証して、メソッドを呼び出します。

アクション フィルターの一覧を取得する

アクションの呼び出し元は IActionInvoker インターフェイスで認識されますが、ASP.NET MVC フレームワークでは、組み込みの ControllerActionInvoker クラスのサービスを使用します。このクラスでは、前述の記述子やアクション フィルターなど、多数の追加のメソッドと機能をサポートしています。

ControllerActionInvoker クラスでは、アクション フィルターを操作するための 2 つの主要な干渉ポイントを提供しています。1 つは InvokeActionMethodWithFilters メソッドです。

protected virtual ActionExecutedContext 

  InvokeActionMethodWithFilters(

  ControllerContext controllerContext, 

  IList<IActionFilter> filters, 

  ActionDescriptor actionDescriptor, 

  IDictionary<string, object> parameters);

もう 1 つは GetFilters メソッドです。

protected virtual FilterInfo GetFilters(

  ControllerContext controllerContext, 

  ActionDescriptor actionDescriptor)

どちらも、ご覧のとおり、保護された仮想メソッドとして宣言されています。

呼び出し元では、特定のアクション用に定義されたフィルターの一覧が必要な場合に GetFilters メソッドを呼び出します。ご推測どおり、この処理は、要求のごく早い段階 (InvokeActionMethodWithFilters メソッドの呼び出しより前の段階) で行われます。

GetFilters メソッドを呼び出すと、呼び出し元では、例外フィルター、結果フィルター、承認フィルター、アクション フィルターを含む、各カテゴリのフィルターの一覧を保持して利用可能な状態を維持します。次のコントローラー クラスについて考えてみましょう。

[HandleError]

public class HomeController : Controller {

  public ActionResult About() {

    return View();

  }

}

クラス全体が HandleError 属性で修飾されています。これは例外フィルターで、これ以外の属性は公開されていません。

今度は、カスタムの呼び出し元を追加し、GetFilters メソッドをオーバーライドし、次のようにコードの最後の行にブレークポイントを設定しましょう。

protected override FilterInfo GetFilters(

  ControllerContext controllerContext, 

  ActionDescriptor actionDescriptor) {



  var filters = base.GetFilters(

    controllerContext, actionDescriptor);

  return filters;

}

図 2 に、さまざまなフィルターの実際のコンテンツを示します。

image: Intercepting the Content of the Filters Collection

図 2 filters コレクションのコンテンツ

FilterInfo クラスでは、カテゴリごとに一連のフィルターを提供します (このクラスは、System.Web.Mvc 名前空間のパブリック クラスです)。

public class FilterInfo {

  public IList<IActionFilter> ActionFilters { get; }

  public IList<IAuthorizationFilter> AuthorizationFilters { get; }

  public IList<IExceptionFilter> ExceptionFilters { get; }

  public IList<IResultFilter> ResultFilters { get; }

  ...

}

図 2 に示した小規模なクラスでは、アクション フィルター、承認フィルター、および結果フィルターは、それぞれ 1 つずつ、例外フィルターは 2 つありました。アクション フィルター、結果フィルター、および承認フィルターは、だれが定義したのかと言うと、コントローラー クラス自体がアクション フィルターになっています。実際、Controller 基本クラスは、すべての関連するフィルター インターフェイスを実装しています。

public abstract class Controller : 

    ControllerBase, IDisposable,

    IActionFilter, IAuthorizationFilter, 

    IExceptionFilter, IResultFilter {

    ...

  }

GetFilters メソッドの基本実装では、.NET Framework のリフレクションを使用して、コントローラー クラスの属性を反映しています。GetFilters メソッドのカスタム実装では、必要な数のフィルターをさまざまな場所から読み込んで追加できます。必要なのは、次のようなコードだけです。

protected override FilterInfo GetFilters(

  ControllerContext controllerContext, 

  ActionDescriptor actionDescriptor) {



  var filters = base.GetFilters(

    controllerContext, actionDescriptor);



  // Load additional filters

  var extraFilters = ReadFiltersFromConfig();

  filters.Add(extraFilters);



  return filters;

}

このアプローチにより、最大限の柔軟性を実現し、あらゆる目標を達成したり、任意の種類のフィルターを追加することができます。

アクションを呼び出す

InvokeActionMethodWithFilters メソッドは、アクション メソッドの実行プロセスで呼び出されます。この場合、メソッドでは、考慮しなければならないアクション フィルターの一覧を受け取ります。ただし、この時点では、まだフィルターを追加することができます。図 3 に、出力を圧縮するアクション フィルターを動的に追加する InvokeActionMethodWithFitleres メソッドのサンプル実装を示します。図 3 のコードでは、まず、呼び出しているメソッドが特定のメソッドかどうかを確認し、メソッドをインスタンス化して、新しいフィルターを追加しています。もちろん、フィルターは、構成ファイル、データベースなど、お望みの場所から読み込めます。InvokeActionMethodWithFilters メソッドをオーバーライドするときに必要なことは、実行中のメソッドをチェックし、追加のアクション フィルターをアタッチし、基本メソッドを呼び出して、呼び出し元が通常通り処理できるようにするだけです。実行中のメソッドに関する情報を取得するには、コントローラーのコンテキストとアクション記述子を使用します。

図 3 アクションの実行前にアクション フィルターを追加する例

protected override ActionExecutedContext 

  InvokeActionMethodWithFilters(

  ControllerContext controllerContext, 

  IList<IActionFilter> filters, 

  ActionDescriptor actionDescriptor, 

  IDictionary<String, Object> parameters) {



  if (

    actionDescriptor.ControllerDescriptor.ControllerName == "Home" 

    && actionDescriptor.ActionName == "About") {



    var compressFilter = new CompressAttribute();

    filters.Add(compressFilter);

  }



  return base.InvokeActionMethodWithFilters(

    controllerContext, 

    filters, actionDescriptor, parameters);

}

つまり、コントローラーのインスタンスにフィルターを動的に追加する方法は 2 つあります。1 つは GetFilters メソッドをオーバーライドする方法で、もう 1 つは InvokeActionMethodWithFilters メソッドをオーバーライドする方法です。ですが、この 2 つの方法は、どこが違うのでしょうか。

アクションのライフサイクル

GetFilters メソッドと InvokeActionMethodWithFilters メソッドのどちらをオーバーライドしても、それほど大きな違いはありません。多少の違いはありますが、それほど大きな違いではありません。違いを理解するために、アクション メソッドを実行するときの既定のアクションの呼び出し元で行われる手順について、少し詳しく見てみましょう。図 4 に、アクション メソッドのライフサイクルの概要を示します。

image: The Lifecycle of an Action Method

図 4 アクション メソッドのライフサイクル

記述子を取得すると、呼び出し元では、フィルターの一覧を取得して、認証フェーズに移行します。この時点では、呼び出し元は、登録されている任意の承認フィルターを使用します。承認に失敗すると、アクションの結果には、どのフィルターも適用されません。

次に、呼び出し元では、投稿されたデータの検証が必要かどうかをチェックし、アクション メソッドの実行に移ります (この際、現在登録されているすべてのフィルターを読み込みます)。

承認フィルターを動的に追加する必要がある場合は、GetFilters メソッドを使用する必要があります。追加しなければならないのがアクション フィルター、結果フィルター、または例外フィルターである場合は、どちらのメソッドを使用しても結果は同じです。

動的なフィルター

フィルターを動的に読み込む機能は、機能が頻繁に変化するアプリケーションで役立つオプションの機能です。フィルター (具体的には、アクション フィルター) を使用すると、開発者は、宣言という形で動作の有効/無効を切り替えられるので、ASP.NET MVC コントローラー クラスのアスペクト指向の機能を実現できます。

コントローラー クラスのソース コードを記述する際には、クラスまたはメソッドのレベルでアクションの属性を追加できます。外部のデータ ソースからアクション フィルターに関する情報を読み取るときに、フィルターとメソッドの相関関係がわかりやすくなるようにする情報の編成方法は明確ではないかもしれません。データベースの場合は、メソッドとコントローラーの名前をキーとして使用するテーブルを作成できます。構成ファイルの場合は、おそらく必要な情報のみを含むカスタムの構成セクションを用意する必要があるでしょう。どちらの場合も、ASP.NET MVC フレームワークは、メソッドまたは呼び出しごとに、適用するフィルターを決められる柔軟性を備えています。

Dino Esposito は、『Programming ASP.NET MVC』(Microsoft Press、2010 年) の著者です。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) で読むことができます。

この記事のレビューに協力してくれた技術スタッフの Scott Hanselman に心より感謝いたします。