通过源代码研究ASP.NET MVC中的Controller和View(五)
2010-11-21 15:35 Ivony... 阅读(4482) 评论(2) 编辑 收藏 举报通过源代码研究ASP.NET MVC中的Controller和View(一)
通过源代码研究ASP.NET MVC中的Controller和View(二)
通过源代码研究ASP.NET MVC中的Controller和View(三)
通过源代码研究ASP.NET MVC中的Controller和View(四)
第五篇,从这一篇开始,将研究ASP.NET的Controller,IController接口是这个样子的:
public interface IController
{
void Execute( RequestContext requestContext );
}
IController是控制器的抽象,由资料可知,当ASP.NET MVC捕获到HTTP请求时,便会通过一系列的机制确定处理当前请求的Controller,创建IController的实例来处理这个请求(RequestContext)。在IController之前的东西,其实是个Routing,或者说请求分发。具体的分发机制与ASP.NET Routing相关,不在我这一次的研究范畴。我们现在假设已经通过分发处理来到了IController,来看看IController的实例是如何处理请求的。
首先通过Reflector看这个接口的实现情况:
很干净的继承链,没有什么旁系和分支,IAsyncController和AsyncController这两个类型从名称来看已经知道大体上应该是用异步处理实现的Controller或IController(就像是IHttpAsyncHandler),不妨看看IAsyncController接口长啥样:
public interface IAsyncController : IController
{
IAsyncResult BeginExecute( RequestContext requestContext, AsyncCallback callback, object state );
void EndExecute( IAsyncResult asyncResult );
}
显然事实就是这样,那么我们只需要关心同步处理的实现(Controller)便可以了,异步处理的逻辑不可能有很大的偏差。
按照一贯的传统,IController接口应该会被抽象基类ControllerBase实现,来看看:
#region IController Members
void IController.Execute( RequestContext requestContext )
{
Execute( requestContext );
}
#endregion
protected virtual void Execute( RequestContext requestContext )
{
if ( requestContext == null )
{
throw new ArgumentNullException( "requestContext" );
}
VerifyExecuteCalledOnce();
Initialize( requestContext );
ExecuteCore();
}
要说明一下这里兜了一个圈子,IController.Execute是一个显示接口实现,当我们将实例当作IController来调用时,会调用到这个方法,但旋即这个方法就调用了ControllerBase.Execute。那么来看Execute方法的实现。
VerifyExecuteCalledOnce,大意是验证Execute是否只被调用一次,一会儿来研究这个方法的实现。然后是初始化(Initialize),最后调用派生类的ExecuteCore方法(因为ExecuteCore是抽象方法)。
初始化的工作非常简单:
protected virtual void Initialize( RequestContext requestContext )
{
ControllerContext = new ControllerContext( requestContext, this );
}
从这里也能看出,ControllerContext = RequestContext + ControllerBase
同时我发现Initialize方法是个虚的,看看派生类是否有篡改,果然:
protected override void Initialize( RequestContext requestContext )
{
base.Initialize( requestContext );
Url = new UrlHelper( requestContext );
}
不过逻辑也非常简单,也只是创建了一个UrlHelper的实例。Execute方法虽然也是虚的,但是Controller并没有篡改,而是老老实实的实现了ExecuteCore。这个一会儿再看,先来研究一下这个VerifyExecuteCalledOnce的实现。话说研究源代码的好处就在于你可以收获许多研究结论之外的东西:
internal void VerifyExecuteCalledOnce()
{
if ( !_executeWasCalledGate.TryEnter() )
{
string message = String.Format( CultureInfo.CurrentUICulture, MvcResources.ControllerBase_CannotHandleMultipleRequests, GetType() );
throw new InvalidOperationException( message );
}
}
调用了一个TryEnter方法,从方法名来看,似乎是进入一个什么状态?临界区?暂时不清楚这个方法和只调用一次的逻辑有什么关系,继续查看源代码:
private readonly SingleEntryGate _executeWasCalledGate = new SingleEntryGate();
// used to synchronize access to a single-use consumable resource
internal sealed class SingleEntryGate
{
private const int NOT_ENTERED = 0;
private const int ENTERED = 1;
private int _status;
// returns true if this is the first call to TryEnter(), false otherwise
public bool TryEnter()
{
int oldStatus = Interlocked.Exchange( ref _status, ENTERED );
return (oldStatus == NOT_ENTERED);
}
}
_executeCalledGate是一个SingleEntryGate的实例,SingleEntryGate的代码也一并列出了。从名称和代码基本上已经可以搞清楚是怎么一回事儿了。
这里的Interlocked.Exchange方法其实就是赋值,只不过是一个原子操作(就是说这个操作只有完成和未完成两种状态,不存在进行中状态),你可以简单的理解为这样的伪代码:
lock ( _status )
{
oldStatus = _status;
_status = ENTERED;
}
当然这个代码是不正确的,因为值类型是不能被lock的,明白大体上是这个意思就行。
其实TryEnter方法上的注释已经写的非常明白了,意思是:如果TryEnter是第一次被调用,那么返回true,否则返回false。
当TryEnter方法第一次被调用时,oldStatus是_status没有被修改之前的默认值也就是0,而_status则会被修改为1(ENTERED),然后比较oldStatus和0(NOT_ENTERED)得到一个true的结果,从而实现这个功能。
那么ControllerBase的Execute逻辑已经清楚了,主要就干了两件事儿,确保Execute方法只被调用一次和准备ControllerContext,然后就把工作交给派生类的ExecuteCore:
protected override void ExecuteCore()
{
// If code in this method needs to be updated, please also check the BeginExecuteCore() and
// EndExecuteCore() methods of AsyncController to see if that code also must be updated.
PossiblyLoadTempData();
try
{
string actionName = RouteData.GetRequiredString( "action" );
if ( !ActionInvoker.InvokeAction( ControllerContext, actionName ) )
{
HandleUnknownAction( actionName );
}
}
finally
{
PossiblySaveTempData();
}
}
方法一开头的注释大体上是告诉开发人员不要忘了还有BeginExecuteCore和EndExecuteCore这回事儿(如果这个方法的代码需要更新,也请检查AsyncController的Begin和EndExecuteCore方法,看看代码是否也必须更新)。
猜测一下,由于IAsyncController的入口不再是Execute,这样ExecuteCore也就不会被调用到,写在ExecuteCore里面的逻辑就应当被写到Begin和EndExecute中去。同样的,ControllerBase的Execute也不会被执行,这部分逻辑恐怕也要写在Begin和EndExecute里面,看了一下源代码,果然不出所料。因为源代码太长,也与今天的研究没啥关系。就不贴了。
看完了注释,接下来是尽可能的(?)加载TempData,最后又有一个尽可能的(?)保存TempData。暂时不明白这个Possibly是咩意思,但加载和保存临时数据还是能明白的,应该就是像ViewState一样的东西,这个与主线逻辑无关,暂时不去探究其实现。
然后是从路由数据中找出actionName,接着InvokeAction,如果返回false(我猜是找不到Action),则处理未知Action。
逻辑非常简单,可以看出来这里又把工作外包给了ActionInvoker去干,总结一下ExecuteCore的逻辑就是:
- 加载临时数据
- 调用action
- 保存临时数据
由于我要追溯的是主线逻辑,所以继续来看ActionInvoker.InvokeAction。ActionInvoker是一个属性:
public IActionInvoker ActionInvoker
{
get
{
if ( _actionInvoker == null )
{
_actionInvoker = CreateActionInvoker();
}
return _actionInvoker;
}
set
{
_actionInvoker = value;
}
}
protected virtual IActionInvoker CreateActionInvoker()
{
return new ControllerActionInvoker();
}
兜了一个圈子,我发现ActionInvoker属性的类型是IActionInvoker,而默认实例是一个ControllerActionInvoker类型的。
IActionInvoker只有一个方法:
public interface IActionInvoker
{
bool InvokeAction( ControllerContext controllerContext, string actionName );
}
那么职责显然是通过actionName调用Action,IActionInvoker的实现类型情况如下:
AsyncControllerActionInvoker和IAsyncActionInvoker应该是异步版本,那么看看ControllerActionInvoker的实现:
public virtual bool InvokeAction( ControllerContext controllerContext, string actionName )
{
if ( controllerContext == null )
{
throw new ArgumentNullException( "controllerContext" );
}
if ( String.IsNullOrEmpty( actionName ) )
{
throw new ArgumentException( MvcResources.Common_NullOrEmpty, "actionName" );
}
ControllerDescriptor controllerDescriptor = GetControllerDescriptor( controllerContext );
ActionDescriptor actionDescriptor = FindAction( controllerContext, controllerDescriptor, actionName );
if ( actionDescriptor != null )
{
FilterInfo filterInfo = GetFilters( controllerContext, actionDescriptor );
try
{
AuthorizationContext authContext = InvokeAuthorizationFilters( controllerContext, filterInfo.AuthorizationFilters, actionDescriptor );
if ( authContext.Result != null )
{
// the auth filter signaled that we should let it short-circuit the request
InvokeActionResult( controllerContext, authContext.Result );
}
else
{
if ( controllerContext.Controller.ValidateRequest )
{
ValidateRequest( controllerContext );
}
IDictionary<string, object> parameters = GetParameterValues( controllerContext, actionDescriptor );
ActionExecutedContext postActionContext = InvokeActionMethodWithFilters( controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters );
InvokeActionResultWithFilters( controllerContext, filterInfo.ResultFilters, postActionContext.Result );
}
}
catch ( ThreadAbortException )
{
// This type of exception occurs as a result of Response.Redirect(), but we special-case so that
// the filters don't see this as an error.
throw;
}
catch ( Exception ex )
{
// something blew up, so execute the exception filters
ExceptionContext exceptionContext = InvokeExceptionFilters( controllerContext, filterInfo.ExceptionFilters, ex );
if ( !exceptionContext.ExceptionHandled )
{
throw;
}
InvokeActionResult( controllerContext, exceptionContext.Result );
}
return true;
}
// notify controller that no method matched
return false;
}
好家伙,大量的代码都在这里了,我们慢慢来分析。
跳过一开始的入口检查,首先是获取两个Descriptor,ControllerDescriptor和ActionDescriptor,如果ActionDescriptor是null,那么返回false,由于ActionDescriptor是由FindAction方法返回,结合调用方的行为,有理由相信这里的逻辑是找不到Action的话就返回false,return false上方的注释也佐证了这一点。
然后从ActionDescriptor获取FilterInfo,从方法名GetFilters来看,FilterInfo应该是一个筛选器的集合。
紧接着进入一个try块,下面的catch逻辑首先是忽略ThreadAbortException(这个对于HTTP处理程序要说是必须的,因为Response.End或Redirect就会产生这个异常),接着其他任何异常都会被捕获,然后InvokeExceptionFilters,这里应该是异常筛选器(关于所有的Filter的内容,主线逻辑完成后我会来做一个总结)。如果异常没有被异常筛选器处理(ExceptionHandled),那么继续抛出,否则InvokeActionResult(猜测这个方法就是调用ActionResult.ExecuteResult)。
核实InvokeActionResult这个猜测很简单,看看源代码:
protected virtual void InvokeActionResult( ControllerContext controllerContext, ActionResult actionResult )
{
actionResult.ExecuteResult( controllerContext );
}
OK,枝节不继续深入,看try里面的情况,首先是调用授权筛选器(InvokeAuthorizationFilters),如果筛选器有结果(推测多半是授权失败之类),那么执行这个结果(InvokeActionResult)。
如果授权部分没有任何结果,那么看看Controller.ValidateRequest是不是true,决定是否进行ValidateRequest,这个ValidateRequest应该是检查XSS威胁之类的,实现如下:
internal static void ValidateRequest( ControllerContext controllerContext )
{
if ( controllerContext.IsChildAction )
{
return;
}
// DevDiv 214040: Enable Request Validation by default for all controller requests
//
// Note that we grab the Request's RawUrl to force it to be validated. Calling ValidateInput()
// doesn't actually validate anything. It just sets flags indicating that on the next usage of
// certain inputs that they should be validated. We special case RawUrl because the URL has already
// been consumed by routing and thus might contain dangerous data. By forcing the RawUrl to be
// re-read we're making sure that it gets validated by ASP.NET.
controllerContext.HttpContext.Request.ValidateInput();
string rawUrl = controllerContext.HttpContext.Request.RawUrl;
}
果然,HttpContext.Request.ValidateInput()。最后的那个rawUrl赋值并不是闲着蛋疼的,上面的注释说了这个原因,大意是:如果不获取RawUrl的值,那么请求验证其实不会真正的被执行,可以认为这是ASP.NET的一个Bug。
继续研究,ValidateRequest之后,调用GetParameterValues方法来获取一个IDictionary<string, object>,这个从名称上来看是获取参数。
然后InvokeActionMethodWithFilters,接着InvokeActionResultWithFilters
InvokeActionResultWithFilters看起来就是InvokeActionResult的WithFilters版本:
protected virtual ResultExecutedContext InvokeActionResultWithFilters( ControllerContext controllerContext, IList<IResultFilter> filters, ActionResult actionResult )
{
ResultExecutingContext preContext = new ResultExecutingContext( controllerContext, actionResult );
Func<ResultExecutedContext> continuation = delegate
{
InvokeActionResult( controllerContext, actionResult );
return new ResultExecutedContext( controllerContext, actionResult, false /* canceled */, null /* exception */);
};
// need to reverse the filter list because the continuations are built up backward
Func<ResultExecutedContext> thunk = filters.Reverse().Aggregate( continuation,
( next, filter ) => () => InvokeActionResultFilter( filter, preContext, next ) );
return thunk();
}
相当的复杂,但我们看到的确是调用了InvokeActionResult,其他的代码大体上是筛选期的逻辑,这些在以后再铺展来谈。我们还是看看InvokeActionMethodWithFilters是不是也调用了InvokeActionMethod然后应用筛选器的逻辑:
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 )
};
// need to reverse the filter list because the continuations are built up backward
Func<ActionExecutedContext> thunk = filters.Reverse().Aggregate( continuation,
( next, filter ) => () => InvokeActionMethodFilter( filter, preContext, next ) );
return thunk();
}
这两个方法的代码几乎是如出一辙(似乎问到了DRY的味道)。好,暂且不管复杂的Filter逻辑,我们赶紧来总结一下ActionInvoker.InvokeAction的流程:
- 获取Controller的描述(Descriptor)
- 查找Action(FindAction)
- 如果找不到Action,那么返回false
- 获取所有的筛选器
- 进入try
- 调用授权筛选
- 如果授权筛选有结果,那么调用授权结果(猜测是授权失败之类)。
- 获取参数
- 调用ActionMethod(InvokeActionMethodWithFilters)
- InvokeActionMethod
- 调用ActionResult(InvokeActionResultWithFilters)
- InvokeActionResult
- ActionResult.ExecuteResult()
- InvokeActionResult
- 调用授权筛选
- 如果try块内有任何不是ThreadAbortedException的异常
- 调用异常筛选
如果我们把这些筛选的逻辑都去掉,则看起来像是这样:
- 查找Action(FindAction)
- 获取参数
- InvokeActionMethod
- InvokeActionResult
这里面 InvokeActionResult我们已经知道是干什么的了,而InvokeActionMethod从名称上来看应该是调用我们写在Controller里面的被称之为Action的方法(例如HomeController.Index等),结合起来上面的GetParameterValues方法就应该是获取这个方法的参数。