iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 22
2

前言

不知道大家有沒有點暈頭轉向XD,MVCModel綁定機制真的蠻複雜,希望大家有跟上來

透過DefaultModelBinderBindComplexElementalModel方法綁定複雜模型的值.

BindProperty方法時填充子節點ModelMetadataModel屬性,透過(DefaultModelBinder)再次綁定物件動作如下

  • ModelMetadata是簡單模型就會把值填充給此次ModelMetadata.Model
  • ModelMetadata是複雜模型就建立一個物件後呼叫BindProperty直到找到最後的簡單模型.

BindComplexElementalModel方法做幾個主要動作

  1. BindProperties:透過MetaData取得屬性資訊並利用反射把值添加上去.
  2. OnModelUpdated:找尋ModelMetaDataModelValidator進行屬性驗證,如果驗證失敗會把資料資訊加到ModelState.AddModelError(ModelStateDictionary)可在View搭配顯示error訊息
internal void BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, object model)
{
    ModelBindingContext newBindingContext = CreateComplexElementalModelBindingContext(controllerContext, bindingContext, model);

    if (OnModelUpdating(controllerContext, newBindingContext))
    {
        BindProperties(controllerContext, newBindingContext);
        OnModelUpdated(controllerContext, newBindingContext);
    }
}

如果前面幾篇看不懂的小夥伴沒關係只要記得,主要透過GetParameterValues方法取得IDictionary<string, objectHttp傳送過來參數綁定到MVC使用Model參數上

  • 字典Key就是Model傳入名稱
  • 字典object就是Model的值

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

Action方法是如何被呼叫(快速整理)

前幾篇有說過InvokeActionMethodWithFilters方法,執行會產生要執行ActionResult物件並使用字典當作參數傳入

InvokeActionMethodWithFilters方法中透過InvokeActionMethod方法來產生要執行的ActionResult

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

protected virtual ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
{
	ActionExecutingContext preContext = new ActionExecutingContext(controllerContext, actionDescriptor, parameters);
	Func<ActionExecutedContext> continuation = () =>
		new ActionExecutedContext(controllerContext, actionDescriptor, false /* canceled */, null /* exception */)
		{
			Result = InvokeActionMethod(controllerContext, actionDescriptor, parameters)
		};

	//preContext 執行前Context   next執行後Context
	Func<ActionExecutedContext> thunk = filters.Reverse().Aggregate(continuation,(next, filter) => () => InvokeActionMethodFilter(filter, preContext, next));
	return thunk();
}

InvokeActionMethod這個方法主要透過ActionDescriptor來回傳此次使用ActionResult物件

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;
}

上面呼叫的是ReflectedActionDescriptor.Execute

ExtractParameterFromDictionary主要透過字典的TryGetValue方法取值(另外還做參數型別驗證)

public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters)
{
	//.....
	ParameterInfo[] parameterInfos = MethodInfo.GetParameters();
	object[] parametersArray = new object[parameterInfos.Length];
	for (int i = 0; i < parameterInfos.Length; i++)
	{
		ParameterInfo parameterInfo = parameterInfos[i];
		object parameter = ExtractParameterFromDictionary(parameterInfo, parameters, MethodInfo);
		parametersArray[i] = parameter;
	}

	ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(MethodInfo);
	object actionReturnValue = dispatcher.Execute(controllerContext.Controller, parametersArray);
	return actionReturnValue;
}

ActionMethodDispatcher 取得(執行Action方法)

ActionMethodDispatcher原始碼能看到在建構子有一個GetExecutor方法(使用Expression表達式產生委派物件).產生ActionExecutor委派物件

裡面有幾個重要的成員

  • ActionExecutor:執行Action方法有回傳值
  • VoidActionExecutor:執行Action方法回傳值是void

透過GetExecutor組成要使用方法委派,等待外部呼叫Execute方法.

internal sealed class ActionMethodDispatcher
{
	private ActionExecutor _executor;

	public ActionMethodDispatcher(MethodInfo methodInfo)
	{
		_executor = GetExecutor(methodInfo);
		MethodInfo = methodInfo;
	}

	private delegate object ActionExecutor(ControllerBase controller, object[] parameters);

	private delegate void VoidActionExecutor(ControllerBase controller, object[] parameters);

	public MethodInfo MethodInfo { get; private set; }

	public object Execute(ControllerBase controller, object[] parameters)
	{
		return _executor(controller, parameters);
	}

	private static ActionExecutor GetExecutor(MethodInfo methodInfo)
	{
		//...
	}

	private static ActionExecutor WrapVoidAction(VoidActionExecutor executor)
	{
		return delegate(ControllerBase controller, object[] parameters)
		{
			executor(controller, parameters);
			return null;
		};
	}
}

前篇有說過在.net原始碼為了確保執行ResultFilter順序在InvokeActionResultWithFilters方法使用遞迴呼叫.

Expression動態產生呼叫Action方法 (GetExecutor)

MVC透過Route機制解析我們要呼叫ControllerAction方法,但在呼叫時動態去判斷要呼叫哪個Action方法,說到動態呼叫方法,有點經驗的人就會想到使用反射(reflection).

反射固然好用,但反射對於效能來說有些不太好(因為要動態到dll metadata找尋取得資訊).

.net MVC工程師也知道上面問題所以這邊他們使用另一種設計方式來避免此問題

使用Expression表達式動態產生呼叫程式碼(也可以使用Emit)並呼叫使用.

UML_Model

先來看看Expreesion產生呼叫HomeControllerIndex方法的程式碼吧.

Expression表達式沒有帶參數Action方法

.Lambda #Lambda1<System.Web.Mvc.ActionMethodDispatcher+ActionExecutor>(
    System.Web.Mvc.ControllerBase $controller,
    System.Object[] $parameters) {
    (System.Object).Call ((Asp.net_MVC_Debuger.Controllers.HomeController)$controller).Index()
}

Expression表達式有帶參數Action方法

.Lambda #Lambda1<System.Web.Mvc.ActionMethodDispatcher+ActionExecutor>(
    System.Web.Mvc.ControllerBase $controller,
    System.Object[] $parameters) {
    (System.Object).Call ((Asp.net_MVC_Debuger.Controllers.HomeController)$controller).Index
	(
		(Asp.net_MVC_Debuger.Models.MessageViewModel)$parameters[0]
	)
}

下面會對於GetExecutor方法透過Expression產生呼叫程式碼解說

GetExecutor方法 Expression產生呼叫程式碼解說

下面是GetExecutor原始碼,讓我一步一步大家分析如何運行吧(介紹Expression表達式和原始碼是如何對照).

private static ActionExecutor GetExecutor(MethodInfo methodInfo)
{
	// Parameters to executor
	ParameterExpression controllerParameter = Expression.Parameter(typeof(ControllerBase), "controller");
	ParameterExpression parametersParameter = Expression.Parameter(typeof(object[]), "parameters");

	// Build parameter list
	List<Expression> parameters = new List<Expression>();
	ParameterInfo[] paramInfos = methodInfo.GetParameters();
	for (int i = 0; i < paramInfos.Length; i++)
	{
		ParameterInfo paramInfo = paramInfos[i];
		BinaryExpression valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
		UnaryExpression valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);

		parameters.Add(valueCast);
	}

	// Call method
	UnaryExpression instanceCast = (!methodInfo.IsStatic) ? Expression.Convert(controllerParameter, methodInfo.ReflectedType) : null;
	MethodCallExpression methodCall = methodCall = Expression.Call(instanceCast, methodInfo, parameters);

	// methodCall is "((TController) controller) method((T0) parameters[0], (T1) parameters[1], ...)"
	// Create function
	if (methodCall.Type == typeof(void))
	{
		Expression<VoidActionExecutor> lambda = Expression.Lambda<VoidActionExecutor>(methodCall, controllerParameter, parametersParameter);
		VoidActionExecutor voidExecutor = lambda.Compile();
		return WrapVoidAction(voidExecutor);
	}
	else
	{
		// must coerce methodCall to match ActionExecutor signature
		UnaryExpression castMethodCall = Expression.Convert(methodCall, typeof(object));
		Expression<ActionExecutor> lambda = Expression.Lambda<ActionExecutor>(castMethodCall, controllerParameter, parametersParameter);
		return lambda.Compile();
	}
}

private static ActionExecutor WrapVoidAction(VoidActionExecutor executor)
{
	return delegate(ControllerBase controller, object[] parameters)
	{
		executor(controller, parameters);
		return null;
	};
}

第一步、先宣告兩個Parameter表達式

  1. controller
  2. parameters:是一個陣列物件

lambda表達式呼叫方法參數

#Lambda1<System.Web.Mvc.ActionMethodDispatcher+ActionExecutor>(
    System.Web.Mvc.ControllerBase $controller,
    System.Object[] $parameters)

第二步、透過for loop建立要傳入Action方法參數陣列

產生完後加入List<Expression>集合中

(Asp.net_MVC_Debuger.Models.MessageViewModel)$parameters[0],
(Asp.net_MVC_Debuger.Models.MessageViewModel1)$parameters[1]
//....

第三步、將controllerParameter強轉型成呼叫使用Controller型別

 ((Asp.net_MVC_Debuger.Controllers.HomeController)$controller)

第四步、使用Expression.Call產生呼叫Action方法動作

(System.Object).Call ((Asp.net_MVC_Debuger.Controllers.HomeController)$controller).Index
(
	(Asp.net_MVC_Debuger.Models.MessageViewModel)$parameters[0]
)

第五步、判斷呼叫方法是否有回傳值(Void),compile成不同程式碼

透過Expression.Lambda將上面程式碼,變成Lambda委派方法提供Execute方法呼叫使用.

.Lambda #Lambda1<System.Web.Mvc.ActionMethodDispatcher+ActionExecutor>(
    System.Web.Mvc.ControllerBase $controller,
    System.Object[] $parameters) {
    (System.Object).Call ((Asp.net_MVC_Debuger.Controllers.HomeController)$controller).Index()
}

能看到上面程式碼如果使用反射可以很輕易完成,但性能就沒有使用Expressionemit來得好

Expression表達式比起emit更簡單了解,所以我會優先使用Expression表達式

DispatcherCache

在取得ActionMethodDispatcher透過一個DispatcherCache屬性.

這是為什麼呢?

ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(MethodInfo);
object actionReturnValue = dispatcher.Execute(controllerContext.Controller, parametersArray);

在上面有分享ActionMethodDispatcher透過Expression表達式產生呼叫方法

Http請求很頻繁,雖然透過Expression表達式動態產生程式碼呼叫比反射效能來好,但一直重複產生程式碼也需要很多效能.

MVC使用一個Cache來保存已經呼叫過資訊DispatcherCache

主要邏輯判斷此MethodInfo是否已經有存入快取字典中.如果沒有建立一個新ActionMethodDispatcher(產生一個新Expression)

internal sealed class ActionMethodDispatcherCache : ReaderWriterCache<MethodInfo, ActionMethodDispatcher>
{
	public ActionMethodDispatcherCache()
	{
	}

	public ActionMethodDispatcher GetDispatcher(MethodInfo methodInfo)
	{
		// Frequently called, so ensure delegate remains static
		return FetchOrCreateItem(methodInfo, (MethodInfo methodInfoInner) => new ActionMethodDispatcher(methodInfoInner), methodInfo);
	}
}

CreateActionResult

CreateActionResult判斷剛剛產生的ActionResult物件進行下面簡單處理

  1. actionReturnValue如果是NULL(回傳值是void)就回傳一個EmptyResult(什麼都不做)
  2. 是否是回傳ActionResult物件,如果不是就利用ContentResult來將結果包起來.
protected virtual ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
{
	if (actionReturnValue == null)
	{
		return new EmptyResult();
	}

	ActionResult actionResult = (actionReturnValue as ActionResult) ??
	new ContentResult { Content = Convert.ToString(actionReturnValue, CultureInfo.InvariantCulture) };
	return actionResult;
}

最後透過ControllerActionInvoker.InvokeActionResult來呼叫ActionResult抽象方法ExecuteResult(ControllerContext context).

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

小結:

本篇介紹了在ReflectedActionDescriptor.Execute方法產生一個ActionResult物件.

ActionMethodDispatcher這個類別負責產生要呼叫ActionResult方法(透過RouteDataActionNmae和反射取得ControllerMethInfo最後透過Expression表達式組成一個呼叫委派方法)

利用DispatcherCache屬性對於每個呼叫過的ActionMethodDispatcher進行快取增加使用效率.

上面使用Expreesion動態產生程式碼並使用Cache這個構想很適合應用在高併發且吃效率情境上.值得我們學習

最後利用CreateActionResult判斷來產生要執行ActionResult

CreateActionResult方法有用到一個設計技巧null object pattern 這個模式用意是為了讓NULL或預設情況也有物件來執行(因為NULL也有屬於它的處理情境)

今天介紹MVC如何運用Expression表達式,對於Expression表達式之後有機會在跟大家做更詳細分享介紹

至於有那些ActionResult可以呼叫我們在下篇會再詳細介紹


上一篇
[Day21] Model 探討驗證標籤(ValidationAttribute)
下一篇
[Day23] 6個基本(ActionResult) View是如何被建立(二)
系列文
從Asp.net框架角度進入Asp.net MVC原始碼30

尚未有邦友留言

立即登入留言