Posted at
C#Day 4

ASP.NET MVCのフィルターについて掘り下げてみた

More than 1 year has passed since last update.

この記事はC# Advent Calendar 2017の4日目の記事です。

今年はASP.NETのカレンダーが無かったので、C#枠を一つお借りして書いてしまおうと思います。

去年投稿したASP.NET MVCのフィルターを触ってみたの続編みたいな話しです。


本日の疑問

MVCのFilterは色々な所で宣言することが可能です。


  • Global.asaxのApplication_Start(FilterConfigの中)で、GlobalFilters.FiltersにAdd

  • コントローラーの属性に設定

  • アクションメソッドの属性に設定

  • コントローラークラスで実装

ところで、複数宣言された場合はどうなるのか。後勝ち?追加??順番は???

気になったので実際に実装して検証してみました。


検証内容


Filterの実装

まず初めに、Traceログを出力するだけの簡単なFilterを作りました。


Controllerに直接実装するパターン

Controller自体がFillterAttributeを継承している為、直接オーバーライドで各フィルターを実装することが可能です。

このController内に存在するアクション全てに適用されるフィルターになります。


HomeController.cs

public class HomeController : Controller

{
// ~ Action実装部省略 ~

protected override void OnAuthorization(AuthorizationContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tController : OnAuthorization");
base.OnAuthorization(filterContext);
}

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tController : OnActionExecuting");
base.OnActionExecuting(filterContext);
}

protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tController : OnActionExecuted");
base.OnActionExecuted(filterContext);
}

protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tController : OnResultExecuting");
base.OnResultExecuting(filterContext);
}

protected override void OnResultExecuted(ResultExecutedContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tControllerBase : OnResultExecuted");
base.OnResultExecuted(filterContext);
}

protected override void OnException(ExceptionContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tController : OnException");
base.OnException(filterContext);
}
}



FilterAttributeを継承しての実装パターン

FilterAttributeの継承と実装したいフィルターのインターフェースを実装したカスタムクラスを用意します。

(今回は検証の為にAllowMultipleを True にしていますが、同じフィルターを複数回適用させない場合は不要です。)


AllFilterAttribute.cs

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]

public class AllFilterAttribute : FilterAttribute, IAuthorizationFilter, IActionFilter, IResultFilter, IExceptionFilter
{
\\ 何処で宣言されたフィルターか分かるように名前を取る
public string Target { get; set; }

public AllFilterAttribute(string target)
{
Target = target;
}

public void OnAuthorization(AuthorizationContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tAllFilterAttribute : OnAuthorization(Target:{Target}/Order:{Order})");
}

public void OnActionExecuted(ActionExecutedContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tAllFilterAttribute : OnActionExecuted(Target:{Target}/Order:{Order})");
}

public void OnActionExecuting(ActionExecutingContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tAllFilterAttribute : OnActionExecuting(Target:{Target}/Order:{Order})");
}

public void OnResultExecuted(ResultExecutedContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tAllFilterAttribute : OnResultExecuted(Target:{Target}/Order:{Order})");
}

public void OnResultExecuting(ResultExecutingContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tAllFilterAttribute : OnResultExecuting(Target:{Target}/Order:{Order})");
}

public void OnException(ExceptionContext filterContext)
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tAllFilterAttribute : OnException(Target:{Target}/Order:{Order})");
}
}



Filterの適用宣言

続いてフィルターをControllerやActionに適用させていきましょう。

Controllerに直接実装するタイプのフィルターについては、記述と宣言が一緒になっているようなものなので

改めて宣言する必要が無い為、省略しています。


FilterConfigで宣言

Global.asaxから呼び出されるRegisterGlobalFiltersで宣言するパターンです。


FilterConfig.cs

public class FilterConfig

{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new AllFilterAttribute("FilterConfig"));
filters.Add(new AllFilterAttribute("FilterConfig") { Order = 1 }); // Orderの順序検証用
}
}


Controller、Actionの属性で宣言

実際にフィルターを使用したいControllerやActionの属性としてフィルタークラスを宣言するパターンです。


HomeController.cs

[AllFilter("Controller")]

[AllFilter("Controller2", Order = 0)] // Orderの順序検証用
public class HomeController : Controller
{
[AllFilter("Action")]
[AllFilter("Action2")]
public ActionResult Index()
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tIndexActionExecute");
return new ContentResult()
{
Content = "OK"
};
}

[AllFilter("Action")]
[AllFilter("Action2")]
public ActionResult Error()
{
Trace.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")}\tErrorActionExecute");
// ExceptionFilter確認用
throw new Exception();
}

// ~ Filter実装部省略 ~
}



検証結果

通常のパターンと例外発生するパターンの両方を実行してみました。

Traceログを見やすくする為に、改行とコメントは後からいれてます。


通常パターン

// 認証Filter

17:01:25.342 Controller : OnAuthorization
17:01:25.343 AllFilterAttribute : OnAuthorization(Target:FilterConfig/Order:-1)
17:01:25.343 AllFilterAttribute : OnAuthorization(Target:Controller/Order:-1)
17:01:25.344 AllFilterAttribute : OnAuthorization(Target:Action/Order:-1)
17:01:25.347 AllFilterAttribute : OnAuthorization(Target:Action2/Order:-1)
17:01:25.349 AllFilterAttribute : OnAuthorization(Target:Controller2/Order:0)
17:01:25.349 AllFilterAttribute : OnAuthorization(Target:FilterConfig/Order:1)

// Action実行前Filter
17:01:25.354 Controller : OnActionExecuting
17:01:25.354 AllFilterAttribute : OnActionExecuting(Target:FilterConfig/Order:-1)
17:01:25.355 AllFilterAttribute : OnActionExecuting(Target:Controller/Order:-1)
17:01:25.355 AllFilterAttribute : OnActionExecuting(Target:Action/Order:-1)
17:01:25.355 AllFilterAttribute : OnActionExecuting(Target:Action2/Order:-1)
17:01:25.356 AllFilterAttribute : OnActionExecuting(Target:Controller2/Order:0)
17:01:25.356 AllFilterAttribute : OnActionExecuting(Target:FilterConfig/Order:1)

// Action処理
17:01:25.366 IndexActionExecute

// Action実行後Filter
17:01:25.367 AllFilterAttribute : OnActionExecuted(Target:FilterConfig/Order:1)
17:01:25.367 AllFilterAttribute : OnActionExecuted(Target:Controller2/Order:0)
17:01:25.367 AllFilterAttribute : OnActionExecuted(Target:Action2/Order:-1)
17:01:25.370 AllFilterAttribute : OnActionExecuted(Target:Action/Order:-1)
17:01:25.370 AllFilterAttribute : OnActionExecuted(Target:Controller/Order:-1)
17:01:25.370 AllFilterAttribute : OnActionExecuted(Target:FilterConfig/Order:-1)
17:01:25.370 Controller : OnActionExecuted

// Result返却前Filter
17:01:25.373 Controller : OnResultExecuting
17:01:25.373 AllFilterAttribute : OnResultExecuting(Target:FilterConfig/Order:-1)
17:01:25.373 AllFilterAttribute : OnResultExecuting(Target:Controller/Order:-1)
17:01:25.374 AllFilterAttribute : OnResultExecuting(Target:Action/Order:-1)
17:01:25.374 AllFilterAttribute : OnResultExecuting(Target:Action2/Order:-1)
17:01:25.378 AllFilterAttribute : OnResultExecuting(Target:Controller2/Order:0)
17:01:25.378 AllFilterAttribute : OnResultExecuting(Target:FilterConfig/Order:1)

// ここで結果が返るはず・・・

// Result返却後Filter
17:01:25.379 AllFilterAttribute : OnResultExecuted(Target:FilterConfig/Order:1)
17:01:25.379 AllFilterAttribute : OnResultExecuted(Target:Controller2/Order:0)
17:01:25.379 AllFilterAttribute : OnResultExecuted(Target:Action2/Order:-1)
17:01:25.381 AllFilterAttribute : OnResultExecuted(Target:Action/Order:-1)
17:01:25.381 AllFilterAttribute : OnResultExecuted(Target:Controller/Order:-1)
17:01:25.381 AllFilterAttribute : OnResultExecuted(Target:FilterConfig/Order:-1)
17:01:25.381 Controller : OnResultExecuted


例外パターン

// 認証Filter

17:05:06.719 Controller : OnAuthorization
17:05:06.720 AllFilterAttribute : OnAuthorization(Target:FilterConfig/Order:-1)
17:05:06.723 AllFilterAttribute : OnAuthorization(Target:Controller/Order:-1)
17:05:06.723 AllFilterAttribute : OnAuthorization(Target:Action/Order:-1)
17:05:06.724 AllFilterAttribute : OnAuthorization(Target:Action2/Order:-1)
17:05:06.724 AllFilterAttribute : OnAuthorization(Target:Controller2/Order:0)
17:05:06.725 AllFilterAttribute : OnAuthorization(Target:FilterConfig/Order:1)

// Action実行前Filter
17:05:06.728 Controller : OnActionExecuting
17:05:06.731 AllFilterAttribute : OnActionExecuting(Target:FilterConfig/Order:-1)
17:05:06.731 AllFilterAttribute : OnActionExecuting(Target:Controller/Order:-1)
17:05:06.732 AllFilterAttribute : OnActionExecuting(Target:Action/Order:-1)
17:05:06.734 AllFilterAttribute : OnActionExecuting(Target:Action2/Order:-1)
17:05:06.750 AllFilterAttribute : OnActionExecuting(Target:Controller2/Order:0)
17:05:06.751 AllFilterAttribute : OnActionExecuting(Target:FilterConfig/Order:1)

// Action処理
17:05:06.752 ErrorActionExecute

// Action実行後Filter
17:05:06.781 AllFilterAttribute : OnActionExecuted(Target:FilterConfig/Order:1)
17:05:06.794 AllFilterAttribute : OnActionExecuted(Target:Controller2/Order:0)
17:05:06.805 AllFilterAttribute : OnActionExecuted(Target:Action2/Order:-1)
17:05:06.814 AllFilterAttribute : OnActionExecuted(Target:Action/Order:-1)
17:05:06.823 AllFilterAttribute : OnActionExecuted(Target:Controller/Order:-1)
17:05:06.832 AllFilterAttribute : OnActionExecuted(Target:FilterConfig/Order:-1)
17:05:06.841 Controller : OnActionExecuted

// 例外Filter
17:05:06.850 AllFilterAttribute : OnException(Target:FilterConfig/Order:1)
17:05:06.851 AllFilterAttribute : OnException(Target:Controller2/Order:0)
17:05:06.851 AllFilterAttribute : OnException(Target:Action2/Order:-1)
17:05:06.854 AllFilterAttribute : OnException(Target:Action/Order:-1)
17:05:06.854 AllFilterAttribute : OnException(Target:Controller/Order:-1)
17:05:06.854 AllFilterAttribute : OnException(Target:FilterConfig/Order:-1)
17:05:06.856 MyHandleErrorAttribute : OnException
17:05:06.858 Controller : OnException


結果まとめ


実行順について

以下の項目を上から順に判定して決められている。


  1. Orderプロパティ

  2. 宣言箇所


    1. Controller

    2. FilterConfig

    3. ControllerでのAttribute

    4. ActionでのAttribute



  3. 宣言順

ただし、昇順 or 降順についてはメソッド毎に異なる。


  • 昇順で実行されるメソッド


    • IAuthorizationFilter.OnAuthorization

    • IActionFilter.OnActionExecuting

    • IResultFilter.OnResultExecuting



  • 降順で実行されるメソッド


    • IActionFilter.OnActionExecuted

    • IResultFilter.OnResultExecuted

    • IExceptionFilter.OnException



昇順と降順については以下のような特徴があるように見える。

メイン処理の「前」に実行されるフィルター処理 → 昇順

メイン処理の「後」に実行されるフィルター処理 → 降順


例外発生時の処理

Actionメソッド内で例外が発生した場合でも、例外発生後にIActionFilter.OnActionExecutedメソッドは実行される為、

ここでActionが成功している前提の処理は書かないほうが無難。

IResultFilterの処理は実行されない為、「結果が返る!」と分かったときの処理はここに集約する。

IExceptionFilter.OnExceptionは実装されている全てが呼び出される。

(今回の実験結果には含まれてないが)Resultの値は後勝ちになるので、注意が必要。