🚫 Ad Blocker Detected

Please disable your AD blocker to continue using this site. Ads help us keep the content free! please press keyboard F5 to refresh page after disabled AD blocker

請關閉廣告攔截器以繼續使用本網站。廣告有助於我們保證內容免費。謝謝! 關閉後請按 F5 刷新頁面

0%

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 許可協議。轉載請註明出處!

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