この記事は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内に存在するアクション全てに適用されるフィルターになります。
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 にしていますが、同じフィルターを複数回適用させない場合は不要です。)
[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
で宣言するパターンです。
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の属性としてフィルタークラスを宣言するパターンです。
[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
結果まとめ
実行順について
以下の項目を上から順に判定して決められている。
- Orderプロパティ
- 宣言箇所
3. Controller
4. FilterConfig
5. ControllerでのAttribute
6. ActionでのAttribute - 宣言順
ただし、昇順 or 降順についてはメソッド毎に異なる。
- 昇順で実行されるメソッド
- IAuthorizationFilter.OnAuthorization
- IActionFilter.OnActionExecuting
- IResultFilter.OnResultExecuting
- 降順で実行されるメソッド
- IActionFilter.OnActionExecuted
- IResultFilter.OnResultExecuted
- IExceptionFilter.OnException
昇順と降順については以下のような特徴があるように見える。
メイン処理の「前」に実行されるフィルター処理 → 昇順
メイン処理の「後」に実行されるフィルター処理 → 降順
例外発生時の処理
Actionメソッド内で例外が発生した場合でも、例外発生後にIActionFilter.OnActionExecuted
メソッドは実行される為、
ここでActionが成功している前提の処理は書かないほうが無難。
IResultFilter
の処理は実行されない為、「結果が返る!」と分かったときの処理はここに集約する。
IExceptionFilter.OnException
は実装されている全てが呼び出される。
(今回の実験結果には含まれてないが)Resultの値は後勝ちになるので、注意が必要。