MVC Filter 機制解密 (第16天)

Agenda

前言

上篇和大家介紹Filter去是如何取得且我們可以透過IOC容器註冊IFilterProvider來擴充取得Filter注入點.

ASP.NET MVCFilter,在執行目標前後彈性擴充額外操作(繼承ActionFilter並掛Attribute),這是一種典型的AOP設計模式

本篇會和大家繼續分享InvokeAction後續動作.

為什麼我們在Action方法和Controller類別放置一個繼承(AuthorizationFilter、ActionFilter、ResultFilter,ExceptionFilter)標籤(Attribute)對應介面(IAuthorizationFilter、IActionFilter、IResultFilter,IExceptionFilter),程式幫我們自動載入MVC生命週期中並執行?

我有做一個可以針對於Asp.net MVC Debugger的專案,只要下中斷點就可輕易進入Asp.net MVC原始碼.

揭密取得過濾器(Filter)機制AOP

AOP 是 OOP(物件導向)一個變化程式撰寫思想。(非取代OOP而是擴充)

導入AOP幫助:

可幫我們分離核心邏輯非核心邏輯代碼,很好降低模組間耦合性,已便日後擴充。

非核心邏輯代碼像:(日誌記錄,性能統計,安全控制,事務處理,異常處理等代碼從業務邏輯代碼中劃分出來)

https://ithelp.ithome.com.tw/upload/images/20180209/20096630UyP6I4l2MB.png

原本寫法把寫日誌相關程式寫入,業務邏輯方法中。導致此方法非單一職則。我們可以把程式重構改寫成(右圖),將寫日誌方法抽離出來更有效達成模組化。

AOP(Aspect-Oriented Programming)核心概念Proxy Pattern

AOP是擴充Proxy Pattern(代理模式)概念,為每個方法提供一個代理人,可為執行前或執行後提供擴展機制,並由代理類別來呼叫真正呼叫使用方法.

如果想要更多了解代理模式可以參考我之前寫的ProxyPattern代理模式(二)

五種過濾器(Filter)介面

Asp.net MVC有五個過濾器實現AOP架構

下面順序案照執行呼叫執行順序來介紹

  1. IAuthenticationFilter:最一開始執行驗證使用過濾器,這個介面有一個void OnAuthentication(AuthenticationContext filterContext)方法.如果驗證失敗可以對於filterContext.Result設值來結束這次請求.
  2. IAuthorizationFilter:執行過程和IAuthenticationFilter過濾器基本上一樣
  3. IActionFilter:提供方法執行前,後的動作.
  4. IResultFilter:提供方法執行結果前,後的動作.
  5. IExceptionFilter:在執行此方法有錯誤時觸發的過濾器.

MVC上面幾個過濾器,讓開發者可以很有彈性擴充自己的系統且不用動到核心原始碼.很好達到開放封閉原則

AuthorizationFilter

AuthorizationFilterActionInvoker執行前第一項工作,因為後續工作(參數模型綁定,參數模型驗證,呼叫方法)只有在驗證成功的基礎上才會有意義。

IAuthenticationFilter and AuthenticationContext

一開始呼叫InvokeAuthenticationFilters方法來取得AuthenticationContext物件,在判斷authenticationContext.Result是否有給值.如果有當作驗證失敗不用在執行後面流程.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try
{
AuthenticationContext authenticationContext = InvokeAuthenticationFilters(controllerContext, filterInfo.AuthenticationFilters, actionDescriptor);

if (authenticationContext.Result != null)
{
AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
authenticationContext.Result);
InvokeActionResult(controllerContext, challengeContext.Result ?? authenticationContext.Result);
}
else
{
//.....
}
}

InvokeAuthenticationFilters方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protected virtual AuthenticationContext InvokeAuthenticationFilters(
ControllerContext controllerContext,
IList<IAuthenticationFilter> filters,
ActionDescriptor actionDescriptor)
{

//....
AuthenticationContext context = new AuthenticationContext(controllerContext, actionDescriptor,
originalPrincipal);
foreach (IAuthenticationFilter filter in filters)
{
filter.OnAuthentication(context);
// short-circuit evaluation when an error occurs
if (context.Result != null)
{
break;
}
}

IPrincipal newPrincipal = context.Principal;

if (newPrincipal != originalPrincipal)
{
Contract.Assert(context.HttpContext != null);
context.HttpContext.User = newPrincipal;
Thread.CurrentPrincipal = newPrincipal;
}

return context;
}

AuthenticationContext中重要的一個屬性是

  • public ActionResult Result { get; set; } 只要這個物件不為null就會直接返回此次請求.

在方法中我封裝一個AuthenticationContext物件,把它當作參數傳入IAuthenticationFilter.OnAuthentication方法中(這就是我們在繼承AuthenticationFilter使用AuthenticationContext物件)

值得一提程式會判斷context.Result是否為null來當作迴圈中斷點.

1
2
3
4
if (context.Result != null)
{
break;
}

這個邏輯是我們對於Authentication驗證失敗後想要直接返回請求可以透過把context.Result給一個值(ActionResult物件),外面會照authenticationContext.Result是否為null為依據判斷是否繼續執行後面動作.

IAuthorizationFilter and AuthorizationContext

下一個步驟是檢驗IAuthorizationFilter過濾器,執行過程和IAuthenticationFilter過濾器基本上一樣

依照物件內Result屬性是否為null來當作後續執行依據.

1
2
3
4
5
6
7
8
9
10
11
12
13
AuthorizationContext authorizationContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
if (authorizationContext.Result != null)
{
AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
authorizationContext.Result);
InvokeActionResult(controllerContext, challengeContext.Result ?? authorizationContext.Result);
}

public interface IAuthorizationFilter
{
void OnAuthorization(AuthorizationContext filterContext);
}

AuthorizationContext類別

1
2
3
4
5
6
7
8
public class AuthorizationContext : ControllerContext
{
//.....

public virtual ActionDescriptor ActionDescriptor { get; set; }

public ActionResult Result { get; set; }
}

既然IAuthenticationFilterIAuthorizationFilter過濾器驗證東西都很類似為什麼要分成兩個呢?

仔細比較會發現IAuthenticationFilter多了(設置Principal),檢驗方式。

ActionDescriptor(使用ReflectedActionDescriptor)這個物件存放目前執行Action相關的資訊(裡面有一個Execute抽象方法,靠他來做Action呼叫使用)

1
2
3
4
protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
{
actionResult.ExecuteResult(controllerContext);
}

如果判斷權限錯誤或Filter需提前返回Result就會執行InvokeActionResult方法,來執行返回工作.

IActionFilter方法執行前,後的過濾器

有在寫Asp.net MVC的人一定對於下面這個介面不陌生,這個過濾器在InvokeActionMethodFilter使用時被呼叫.

ActionExecutingContext也有一個Result物件用此判斷是否有執行後續請求.一般也是NULL

ActionExecutingContext這個物件比其他過濾器參數多了一個重要的成員IDictionary<string, object> parameters,有這個成員我們可以針對呼叫Action參數處理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public interface IActionFilter
{
void OnActionExecuting(ActionExecutingContext filterContext);
void OnActionExecuted(ActionExecutedContext filterContext);
}

internal static ActionExecutedContext InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func<ActionExecutedContext> continuation)
{
//執行Action 過濾器
filter.OnActionExecuting(preContext);
//如果有Result 直接返回
if (preContext.Result != null)
{
return new ActionExecutedContext(preContext, preContext.ActionDescriptor, true /* canceled */, null /* exception */)
{
Result = preContext.Result
};
}

bool wasError = false;
ActionExecutedContext postContext = null;
try
{
postContext = continuation();
}
catch (ThreadAbortException)
{
postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, null /* exception */);
//執行Action後 過濾器
filter.OnActionExecuted(postContext);
throw;
}
catch (Exception ex)
{
wasError = true;
postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, ex);
filter.OnActionExecuted(postContext);
if (!postContext.ExceptionHandled)
{
throw;
}
}
if (!wasError)
{
filter.OnActionExecuted(postContext);
}
return postContext;
}

其中有一段continuation這個委派是InvokeActionMethod這個方法,這個方法取得使用Action方法.

1
2
3
4
5
6
protected virtual ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
{
object returnValue = actionDescriptor.Execute(controllerContext, parameters);
ActionResult result = CreateActionResult(controllerContext, actionDescriptor, returnValue);
return result;
}
1
2
3
4
try
{
postContext = continuation();
}

ActionExecutedContext物件中的Result屬性就是執行Action方法後的結果

InvokeActionResult 動作執行前,後過濾器

呼叫InvokeActionResult過濾器藉由InvokeActionResultFilterRecursive方法

這個方法使用遞迴方式看之前的使用for loop執行過濾器方式有所不同,幸好在原始碼有註解.

主要是因為下面原因

OnResultExecuting事件必須按正向順序觸,發然後必須觸發InvokeActionResult(執行Action動作方法),OnResultExecuted事件必須以相反的順序觸發

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private ResultExecutedContext InvokeActionResultFilterRecursive(IList<IResultFilter> filters, int filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult)
{
if (filterIndex > filters.Count - 1)
{
InvokeActionResult(controllerContext, actionResult);
return new ResultExecutedContext(controllerContext, actionResult, canceled: false, exception: null);
}

IResultFilter filter = filters[filterIndex];
filter.OnResultExecuting(preContext);
if (preContext.Cancel)
{
return new ResultExecutedContext(preContext, preContext.Result, canceled: true, exception: null);
}

bool wasError = false;
ResultExecutedContext postContext = null;
try
{
int nextFilterIndex = filterIndex + 1;
postContext = InvokeActionResultFilterRecursive(filters, nextFilterIndex, preContext, controllerContext, actionResult);
}
catch (ThreadAbortException)
{
postContext = new ResultExecutedContext(preContext, preContext.Result, canceled: false, exception: null);
filter.OnResultExecuted(postContext);
throw;
}
catch (Exception ex)
{
wasError = true;
postContext = new ResultExecutedContext(preContext, preContext.Result, canceled: false, exception: ex);
filter.OnResultExecuted(postContext);
if (!postContext.ExceptionHandled)
{
throw;
}
}
if (!wasError)
{
filter.OnResultExecuted(postContext);
}
return postContext;
}

OnResultExecuting方法的ResultExecutingContext可以藉由Canceled這個屬性來最後控制是否要執行Action方法,如果不要將這個值設定為false.

1
public virtual bool Canceled { get; set; }

IExceptionFilter錯誤過濾器

最後介紹錯誤時呼叫的過濾器IExceptionFilter

可以看到在執行方法的最前面使用了一個try....catch而最後catch程式碼如下.

在這個方法中有一個重要的屬性是bool ExceptionHandled,如果在錯誤時設定為true她就會執行Result的結果(因為最後呼叫了InvokeActionResult方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//....
catch (Exception ex)
{
// 錯誤處理過濾器
ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
//如果需要自己處理錯誤 exceptionContext.ExceptionHandled 設為true
if (!exceptionContext.ExceptionHandled)
{
throw;
}
InvokeActionResult(controllerContext, exceptionContext.Result);
}

protected virtual ExceptionContext InvokeExceptionFilters(ControllerContext controllerContext, IList<IExceptionFilter> filters, Exception exception)
{
ExceptionContext context = new ExceptionContext(controllerContext, exception);
foreach (IExceptionFilter filter in filters.Reverse())
{
filter.OnException(context);
}

return context;
}

小結:

過濾器這部分原始碼很值得大家探討,因為在主流IOC容器框架有支援AOP概念.

AOP有很大優點是可做到設計五大原則的其中兩項

  • 單一職責原則
  • 開放封閉原則

使程式碼耦合性變低

執行Action方法前,如何取得權限過濾器並呼叫檢驗,另外在呼叫方法前可以看到會把用到的資訊封裝到一個Context物件中.

IAuthenticationFilterIAuthorizationFilter基本上都是權限驗證的過濾器

但有先後順序,這點需注意!! 先執行IAuthenticationFilterIAuthorizationFilter

看了MVC過濾器原始碼後有感而法,石頭就基於RealProxy這個類別做了一個AOP開源框架AwesomeProxy.Net.

下篇會繼續介紹Action參數如何建立,遇到複雜Model MVC是怎麼處理

此文作者:Daniel Shih(石頭)
此文地址https://isdaniel.github.io/ithelp-day16/
版權聲明:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!


如果本文對您幫助很大,可街口支付斗內鼓勵石頭^^