iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 16
1
Software Development

從Asp.net框架角度進入Asp.net MVC原始碼系列 第 16

[Day16] MVC Filter 機制解密

## 前言

上篇和大家介紹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是否有給值.如果有當作驗證失敗不用在執行後面流程.

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方法

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來當作迴圈中斷點.

if (context.Result != null)
{
    break;
}

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

IAuthorizationFilter and AuthorizationContext

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

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

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類別

public class AuthorizationContext : ControllerContext
{
	//.....

	public virtual ActionDescriptor ActionDescriptor { get; set; }

	public ActionResult Result { get; set; }
}

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

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

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

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參數處理.

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方法.

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;
}
try
{
	postContext = continuation();
}

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

InvokeActionResult 動作執行前,後過濾器

呼叫InvokeActionResult過濾器藉由InvokeActionResultFilterRecursive方法

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

主要是因為下面原因

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

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.

public virtual bool Canceled { get; set; }

IExceptionFilter錯誤過濾器

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

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

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

//....
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是怎麼處理


上一篇
[Day15] Action方法如何被執行InvokeAction(一)
下一篇
[Day17] Action方法如何被執行InvokeAction(二)
系列文
從Asp.net框架角度進入Asp.net MVC原始碼30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言