normalian blog

Let's talk about Microsoft Azure, ASP.NET and Java!

ASP.NET MVCにおけるFilterの作成方法と実行順序制御

フィルタって?

ASP.NET MVCでは、「System.Web.Mvc.Controllerクラスを継承した自作コントローラークラス」がロジックの中核を担います。以下の例はデフォルト生成されたコントロールクラスです。

    public class HomeController : Controller
    {
        public ActionResult Index(string comment)
        {
            ViewData["Message"] = "Hello World的な何か";
            return View();
        }
        (中略)
    }

この例では、単純に"<ドメイン名>/Home/"にアクセスした場合にViewDataにデータをつめて返すだけの処理を実装しています。このアクション実行前後に汎用的な処理を行いたい場合、ASP.NET MVCの機能であるフィルターを実行する事で実現する事が可能です。Java出身者な人たちには「サーブレットフィルタみたいな奴だよ」とか言えばわかって貰えるかも知れません。

どうやって動作させる?

フィルタはコントローラに対して、属性として付与する事が可能です。具体的には以下な感じで付与できます。

    [SampleClassFilter]
    public class DummyController : Controller
    {
        [SampleMethodFilter]
        public ActionResult Index(string comment)
        {
            if (string.IsNullOrEmpty(comment) == false)
            {
                ViewData["Message"] = "送信したコメント : " + comment;
            }
            return View();
        }

        public ActionResult List()
        {
            return View();
        }
        (中略)
    }

この例で、SampleClassFilterはクラス全体に対して付与し、SampleMethodFilterはメソッドに対してのみ付与しています。従って、以下の様な感じ

  • Indexアクション実行時
    • SampleClassFilter、SampleMethodFilter両方実行
  • Listアクション実行時
    • SampleClassFilterのみ実行

Filterの作り方

属性として定義するので、以下のクラスを継承する必要があります。

  • 必須クラス
    • FilterAttribute
  • 任意クラス
    • IAuthorizationFilter(認証前に処理を追加)
    • IActionFilter(アクション実行前後に処理を追加)
    • IResultFilter(ActionResult#ExecuteResult実行前後に処理を追加)
    • IExceptionFilter(例外発生時の処理を追加)

サンプルコードは以下になります。フィルターを「クラスのみ、メソッドのみ」とそれぞれ付与を制御する為には、AttributeUsage属性の引数で制御します。

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
    public class CustomRapidFilterAttribute : FilterAttribute, IAuthorizationFilter, IActionFilter, IResultFilter, IExceptionFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            Trace.WriteLine("Filter1:OnAuthorization()" + filterContext.HttpContext.User.Identity.Name);
        }

        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            Trace.WriteLine("Filter1:OnActionExecuting()");
        }

        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
            Trace.WriteLine("Filter1:OnActionExecuted()");
        }

        public void OnResultExecuting(ResultExecutingContext filterContext)
        {
            Trace.WriteLine("Filter1:OnResultExecuting()");
        }

        public void OnResultExecuted(ResultExecutedContext filterContext)
        {
            Trace.WriteLine("Filter1:OnResultExecuted()");
        }

        public void OnException(ExceptionContext filterContext)
        {
            Trace.WriteLine("Filter1:OnException()");
        }
    }

順序制御

Filter一個での実装・動作に関しては Usa*Usaさんのはてだ d:id:machi_pon:20090405:1238857545 を見ていただくとして、複数のFilterが有った場合についてです。実は順番制御の為にSystem.Web.Mvc.FilterAttributeにOrderという拡張ポイントが用意されております。以下はFilterAttributeの実装を抜粋。

    public abstract class FilterAttribute : Attribute {
        private int _order = -1;
        public int Order {
            get {
                return _order;
            }
            set {
                if (value < -1) {
                    throw new ArgumentOutOfRangeException("value",
                        MvcResources.FilterAttribute_OrderOutOfRange);
                }
                _order = value;
            }
        }
    }

Orderの値の大きさによって順序制御をする事になっており、デフォルト値は-1、-1より小さい値を設定すると例外を吐く設定になっています。

動作確認

後で示すサンプルコードをもとに、実際にフィルターを実行した際のログを提示します。

HomeController:OnAuthorization()
Filter2:OnAuthorization()
Filter1:OnAuthorization()
HomeController:OnActionExecuting()
Filter2:OnActionExecuting()
Filter1:OnActionExecuting()
//実はここでアクションを実行している
Filter1:OnActionExecuted()
Filter2:OnActionExecuted()
HomeController:OnActionExecuted()
HomeController:OnResultExecuting()
Filter2:OnResultExecuting()
Filter1:OnResultExecuting()
//ここでActionResult#ExecuteResult実行
Filter1:OnResultExecuted()
Filter2:OnResultExecuted()
HomeController:OnResultExecuted()

ごらんの通り、値が大きいほど先に実行されるという優先順位になっています。XXXXExecutedメソッドに関しては実行順序が逆になっていますので、その点には気をつける必要があります。

サンプルのコード

    [CustomRapidFilter]
    [CustomLateFilter]
    public class HomeController : Controller
    {
        //このアクション実行時に”CustomRapidFilter”、”CustomLateFilter”が実行される
        public ActionResult Index(string comment)
        {
            ViewData["Message"] = "Hello World的な何か";
            return View();
        }
        (中略)
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
    public class CustomRapidFilterAttribute : FilterAttribute, IAuthorizationFilter, IActionFilter, IResultFilter, IExceptionFilter
    {
        public CustomRapidFilterAttribute()
        {
            // この値で順序制御。大きいほうが優先度が高い
            Order = 30;
        }

        public void OnAuthorization(AuthorizationContext filterContext)
        {
            Trace.WriteLine("Filter1:OnAuthorization()" + filterContext.HttpContext.User.Identity.Name);
        }

        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            Trace.WriteLine("Filter1:OnActionExecuting()");
        }

        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
            Trace.WriteLine("Filter1:OnActionExecuted()");
        }

        public void OnResultExecuting(ResultExecutingContext filterContext)
        {
            Trace.WriteLine("Filter1:OnResultExecuting()");
        }

        public void OnResultExecuted(ResultExecutedContext filterContext)
        {
            Trace.WriteLine("Filter1:OnResultExecuted()");
        }

        public void OnException(ExceptionContext filterContext)
        {
            Trace.WriteLine("Filter1:OnException()");
        }
    }

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
    public class CustomLateFilterAttribute : FilterAttribute, IAuthorizationFilter, IActionFilter, IResultFilter, IExceptionFilter
    {
        public CustomLateFilterAttribute()
        {
            // この値で順序制御。大きいほうが優先度が高い
            Order = 20;
        }

        public void OnAuthorization(AuthorizationContext filterContext)
        {
            Trace.WriteLine("Filter2:OnAuthorization()");
        }

        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            Trace.WriteLine("Filter2:OnActionExecuting()");
        }

        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
            Trace.WriteLine("Filter2:OnActionExecuted()");
        }

        public void OnResultExecuting(ResultExecutingContext filterContext)
        {
            Trace.WriteLine("Filter2:OnResultExecuting()");
        }

        public void OnResultExecuted(ResultExecutedContext filterContext)
        {
            Trace.WriteLine("Filter2:OnResultExecuted()");
        }

        public void OnException(ExceptionContext filterContext)
        {
            Trace.WriteLine("Filter2:OnException()");
        }
    }