讓人摸不透的Asp.Net Mvc Filter

在某個工作中,需要在一個Request進到Action前攔下來做一些前置的邏輯判斷,直覺地想到應該就寫個CustomActionFilter就搞定收工,結果當Filter寫完要放置到流程中的時候發現,哎呀,系統已經存在不少ActionFilter,那我放進去的CustomActionFilter會在哪個階段執行咧?

怎麼說Asp.Net Mvc Filter讓人摸不透呢?其實,只要是在講各種Filter的執行時機點。每一種類型的Filter執行時機點都不一樣。並且,除了Filter的類型會影響執行的順序以外,FilterAttribute還提供了Order屬性,讓使用者來控制在同一個階層的Filter順序。

先來了解一下Asp.Net Mvc提供了那些Filter

  • Authorization filters

  • Action filters

  • Result filters

  • Exception filters

以上總共有四種Filter的類型,而每一種類型的執行時機也都不一樣。此外,Asp.Net Mvc也定義了有些Filter只能註冊和執行在特定的作用域。有些可以註冊在不同的作用域,也會因為註冊在不同的作用域而有不同的執行順序。以下是Asp.Net Mvc所定義的五個作用域

  1. First
  2. Global
  3. Controller
  4. Action
  5. Last

從字面上不難了解作用域在一個Request Life cycle的位置,除了First和Last。那這兩個分別是指什麼呢,我們可以從微軟的MSDN說明得到答案。

FilterScope

在文件上,明確的說明了Authorization Filter就是執行在First,Exception Filter就會執行在Last。其實,這也是很合理的。各位可以思考一下,當我們要做權限驗證時,應該是Request一進到系統時,就必須先驗證是否合法。所以,Authorization Filter執行在First是完全合情合理的。另外,Exception Filter一定是在當系統發生Exception時,會將Request轉到這一個類別中執行一些發生異常時的應對方式,像是紀錄Log後,再回復Response,所以Exception Filter在Last也只是剛剛好而已。

作用域的原文為"FilterScope",在此筆者將它翻成"作用域",如有更好的翻譯名詞,歡迎討論。

除了Authorization Filter和Exception Filter是由特定的作用域決定Filter執行順序外,Action Filter和Result Filter這兩個類型也有先後順序的關係。 在談Action Filter和Result Filter的先後關係前,先來看看這兩個類別分別提供了甚麼方法

  • OnActionExecuting
  • OnActionExecuted
  • OnResultExecuting
  • OnResultExecuted

Executing與Executed的差別在哪呢?主要是Executing是在執行在附加類別之前,Executed則是在附加類別之後。舉個例子來說

[CustomAttribute1]
public ActionResult Index()
{
    return View();
}

CustomAttribute1的Executing就會在進到Action前執行,而Executed則會在執行完Action後執行。現在,再來看看Action Filter與Result Filter,從字面上來看,Result Filter就是在傳回Result的前後時執行,像是ViewResult。Action Filter就是在Controller或是Action前後執行(看Filter註冊位置及使用的Method)。所以,Action Filter一定會比Result Filter先執行。

討論完了因為作用域而產生的順序後,在來探討一下如果在同一個作用域時,該如何決定順序呢?在FilterAttribute類別中,提供了Order屬性來決定在同一個作用域的順序。Asp.Net Mvc會依據Order的大小,由小到大的將同一個作用域下的Filter排序,並依照此順序執行。說了這麼多,實際來看看實際執行起來的結果吧

先定義三個CustomActionFilter,並且在執行的時候,在Response上加上CustomActionFilter的標記

public class CustomAttribute1 : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.HttpContext.Response.Write("CustomAttribute1<br />");
    }
}

public class CustomAttribute2 : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.HttpContext.Response.Write("CustomAttribute2<br />");
    }
}

public class CustomAttribute3 : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutedContext filterContext)
    {
        filterContext.HttpContext.Response.Write("CustomAttribute3<br />");
    }
}

然後再Global上註冊CustomAttribute2

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new CustomAttribute2());
    }
}

最後在Action上註冊CustomAttribute1、CustomAttribute3,並且都不設定Order屬性值

[CustomAttribute3]
[CustomAttribute1]
public ActionResult Index()
{
    return View();
}

執行結果

可以看到註冊在Global的CustomAttribute2在最上面,正確。並且因為都沒有指定順序,所以系統就這著程式碼順序執行。不過兩個自訂的Filter皆沒有指定Order也意味著應該是不在乎兩個Filter的順序,所以讓系統自行排序也沒有錯。

再來,將兩個Action Filter指定順序

[CustomAttribute3(Order = 2)]
[CustomAttribute1(Order = 1)]
public ActionResult Index()
{
    return View();
}

結果

可以發現Action Filter的確是照著程式所指定的Order順序在執行。那如果將程式改成一個指定一個沒指定呢?當初,筆者就是因為這一個情境才會來研究Asp.Net Mvc Filter的順序,將程式改成

[CustomAttribute3(Order = 1)]
[CustomAttribute1]
public ActionResult Index()
{
    return View();
}

到底是CustomAttribute3先執行還是CustomAttribute1先執行呢?(先小小聲地說,最初筆者是認為應該是有指定順序的先執行),來看答案吧。

竟然是CustomAttribute1先執行了,竟然沒指定的會比有指定Order先執行(套一句我常聽到一位Odde的Agile coach口頭禪,這不make sense阿)。所以,來看看MSDN上的解釋吧

可以看到MSDN上解釋,當沒有指定Order屬性值時,預設值為-1。阿.......原來這樣一切就都合理了,因為-1一定是最小的,所以最先執行。

結論

Asp.Net Mvc Filter的順序,決定的因素有很多,根據作用域的不同、Filter類別不同、使用的Method不同以及Order的不同,皆會影響到Filter執行的順序,所以當使用Filter又有順序考量時,必須思考一下。雖然這是一個小小的點。但是,有可能因為執行的順序與預想的不一致,就會導致結果差了十萬八千里。

參考資料

免責聲明:

"文章一定有好壞,文章內容有對有錯,使用前應詳閱公開說明書"