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

この記事は[C# Advent Calendar 2017](https://qiita.com/advent-calendar/2017/csharp)の4日目の記事です。

今年はASP.NETのカレンダーが無かったので、C#枠を一つお借りして書いてしまおうと思います。
去年投稿した[ASP.NET MVCのフィルターを触ってみた](https://qiita.com/xxIN0xx/items/aa960db70b3b81ac98f5)の続編みたいな話しです。

# 本日の疑問
MVCのFilterは色々な所で宣言することが可能です。

- Global.asaxのApplication_Start(FilterConfigの中)で、GlobalFilters.FiltersにAdd
- コントローラーの属性に設定
- アクションメソッドの属性に設定
- コントローラークラスで実装

ところで、複数宣言された場合はどうなるのか。後勝ち？追加？？順番は？？？
気になったので実際に実装して検証してみました。

# 検証内容
## Filterの実装
まず初めに、Traceログを出力するだけの簡単なFilterを作りました。

### Controllerに直接実装するパターン
Controller自体がFillterAttributeを継承している為、直接オーバーライドで各フィルターを実装することが可能です。
このController内に存在するアクション全てに適用されるフィルターになります。

```C#: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 にしていますが、同じフィルターを複数回適用させない場合は不要です。）

```C#: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`で宣言するパターンです。

```C#: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の属性としてフィルタークラスを宣言するパターンです。

```C#: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. 宣言箇所
    3. Controller
    4. FilterConfig
    5. ControllerでのAttribute
    6. ActionでのAttribute
7. 宣言順

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

- 昇順で実行されるメソッド
    - IAuthorizationFilter.OnAuthorization
    - IActionFilter.OnActionExecuting
    - IResultFilter.OnResultExecuting
- 降順で実行されるメソッド
    - IActionFilter.OnActionExecuted
    - IResultFilter.OnResultExecuted
    - IExceptionFilter.OnException

昇順と降順については以下のような特徴があるように見える。
メイン処理の「前」に実行されるフィルター処理 → **昇順**
メイン処理の「後」に実行されるフィルター処理 → **降順**

## 例外発生時の処理
Actionメソッド内で例外が発生した場合でも、例外発生後に`IActionFilter.OnActionExecuted`メソッドは実行される為、
ここでActionが成功している前提の処理は書かないほうが無難。

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

`IExceptionFilter.OnException`は実装されている全てが呼び出される。
（今回の実験結果には含まれてないが）Resultの値は後勝ちになるので、注意が必要。