この記事は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の値は後勝ちになるので、注意が必要。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.