ASP.NET MVC - FilterAttribute.OrderとFilterScopeによるフィルタの実行順を確認する

FilterAttribute.OrderとFilterScopeによるフィルタの実行順を確認します。

同じフィルタが複数ある場合に

  1. FilterAttributeのOrderプロパティの昇順(小さい値から大きい値の順序)
  2. FilterScopeの昇順

でフィルタがソートされて実行される、というあたりを確認します。

FilterAttribute.Order

Orderプロパティは想像しやすいかなと思います。

なので略。

FilterScope

FilterScopeはMVC内部で使われているもので、使う側が指定するものではないようです。

ざっくり言うとフィルタの指定した場所によって決まるものという解釈でいいのかなと思います。

  • FilterScope.First・・・コントローラ自体のフィルタ
  • FilterScope.Global・・・グローバルフィルタに登録したフィルタ
  • FilterScope.Controller・・・コントローラのクラスに指定したFilterAttribute
  • FilterScope.Action・・・アクションメソッドに指定したFilterAttribute
  • FilterScope.Last・・・定義されているだけで使われていないような・・・?

といった感じです。昇順もこの順番です。

FilterScope.Firstを少し補足します。

コントローラは各フィルタのインターフェイスを実装しています。メソッドをオーバーライドすることによってフィルタの処理を書くことができます。このときコントローラ自体のスコープはFilterScope.Firstとなります。またOrderはInt32.MinValueとして扱われているようです。つまりコントローラ自体のフィルタ(オーバーライドしたメソッド)は必ず最初に実行されることになります。

サンプルで確認

ここからはアクションフィルタに絞ってサンプルコードで確認してみます。

まずはアクションフィルタを実装したフィルタ属性を用意します。この属性はトレース出力だけしています。

// アクションフィルタだけを実装したフィルタ属性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class TestFilterAttribute : FilterAttribute, IActionFilter {
    public string Label { get; set; } // 確認用に出力する文字列

    public void OnActionExecuting(ActionExecutingContext filterContext) {
        // Orderとラベルをトレース
        Trace.WriteLine($"{nameof(TestFilterAttribute)}({Order}, \"{Label}\").{nameof(OnActionExecuting)}");
    }

    public void OnActionExecuted(ActionExecutedContext filterContext) {
        Trace.WriteLine($"{nameof(TestFilterAttribute)}({Order}, \"{Label}\").{nameof(OnActionExecuted)}");
    }
}

次に用意した属性をグローバルフィルタに設定します。Orderを省略した場合は-1になります。

// グローバルフィルタとして属性追加(ラベル:"Global")
// Orderを指定しない場合は-1
GlobalFilters.Filters.Add(new TestFilterAttribute { Label = "Global" });
GlobalFilters.Filters.Add(new TestFilterAttribute { Order = 1, Label = "Global" });
GlobalFilters.Filters.Add(new TestFilterAttribute { Order = 2, Label = "Global" });

さらに用意した属性をコントローラのクラスとメソッドにも指定します。またコントローラ自体のアクションフィルタもオーバーライドしてトレースするようにしておきます。

// クラスに属性(ラベル:"Controller")
[TestFilter(Label = "Controller")]
[TestFilter(Order = 1, Label = "Controller")]
[TestFilter(Order = 2, Label = "Controller")]
public class DefaultController : Controller {
    // コントローラ自体のアクションフィルタ
    protected override void OnActionExecuting(ActionExecutingContext filterContext) {
        Trace.WriteLine($"{nameof(DefaultController)}.{nameof(OnActionExecuting)}");
    }
    protected override void OnActionExecuted(ActionExecutedContext filterContext) {
        Trace.WriteLine($"{nameof(DefaultController)}.{nameof(OnActionExecuted)}");
    }

    // メソッドに属性(ラベル:"Action")
    [TestFilter(Label = "Action")]
    [TestFilter(Order = 1, Label = "Action")]
    [TestFilter(Order = 2, Label = "Action")]
    public ActionResult Index() {
        Trace.WriteLine($"{nameof(DefaultController)}.{nameof(Index)}");
        return new EmptyResult();
    }
}

そしてデバッグ実行すると出力結果は次のように。わかりやすいように改行しつつコメントも足しています。

// コントローラ自体のメソッドが真っ先に呼ばれる
DefaultController.OnActionExecuting

// Order = -1(Orderを指定しない)のアクションフィルタ
// Global、Controller、Actionの順で実行される
TestFilterAttribute(-1, "Global").OnActionExecuting
TestFilterAttribute(-1, "Controller").OnActionExecuting
TestFilterAttribute(-1, "Action").OnActionExecuting

// Order = 1のアクションフィルタ
TestFilterAttribute(1, "Global").OnActionExecuting
TestFilterAttribute(1, "Controller").OnActionExecuting
TestFilterAttribute(1, "Action").OnActionExecuting

// Order = 2のアクションフィルタ
TestFilterAttribute(2, "Global").OnActionExecuting
TestFilterAttribute(2, "Controller").OnActionExecuting
TestFilterAttribute(2, "Action").OnActionExecuting

// ここでアクションメソッドの呼び出し
DefaultController.Index

// OnActionExecutedは逆順
TestFilterAttribute(2, "Action").OnActionExecuted
TestFilterAttribute(2, "Controller").OnActionExecuted
TestFilterAttribute(2, "Global").OnActionExecuted

TestFilterAttribute(1, "Action").OnActionExecuted
TestFilterAttribute(1, "Controller").OnActionExecuted
TestFilterAttribute(1, "Global").OnActionExecuted

TestFilterAttribute(-1, "Action").OnActionExecuted
TestFilterAttribute(-1, "Controller").OnActionExecuted
TestFilterAttribute(-1, "Global").OnActionExecuted

DefaultController.OnActionExecuted

Orderの昇順、Orderが同じ場合はグローバルフィルタ、クラスのフィルタ属性、メソッドのフィルタ属性の順(FilterScopeの昇順)で実行されることを確認できました。さらにOnActionExecutingは上記の実行順で実行され、OnActionExecutedはその逆順で実行されることも確認できました。

ちなみにOrderもFilterScopeも同じ場合の実行順は未定義とのことです。

種類、順序、スコープが同じフィルターの実行順序は未定義です。

おしまい。