代码改变世界

通过源代码研究ASP.NET MVC中的Controller和View(五)

2010-11-21 15:35  Ivony...  阅读(4467)  评论(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看这个接口的实现情况:

image

很干净的继承链,没有什么旁系和分支,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的实现类型情况如下:

image

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()
  • 如果try块内有任何不是ThreadAbortedException的异常
    • 调用异常筛选

如果我们把这些筛选的逻辑都去掉,则看起来像是这样:

  • 查找Action(FindAction)
  • 获取参数
  • InvokeActionMethod
  • InvokeActionResult

这里面 InvokeActionResult我们已经知道是干什么的了,而InvokeActionMethod从名称上来看应该是调用我们写在Controller里面的被称之为Action的方法(例如HomeController.Index等),结合起来上面的GetParameterValues方法就应该是获取这个方法的参数。