技术开发 频道

为ASP.NET MVC扩展异步Action功能

  【IT168 技术文档】异步请求处理是ASP.NET 2.0中引入的高级特性,它依托IO Complete Port,对于提高IO密集型应用程序的吞吐量非常重要(详见原理描述和性能测试)。但是目前ASP.NET MVC框架缺少异步Action功能,这也就是老赵经常挂在嘴边的那个“目前ASP.NET MVC所缺少的非常重要的功能”。在TechED 2008 China的Session中我曾经给出过一个所谓的“解决方案”,但是它复杂性之高使那个解决方案有太多限制。

  为了弥补TechED上的遗憾,以及准备.NET开发大会上的ASP.NET MVC非常好的实践的Session,我在春节休假期间仔细思考了一下这方面的问题,得出了一个相对不错的扩展:完整,方便,并且非常轻巧——核心逻辑代码只有200行左右,这意味着绝大部分功能将会委托给框架中现成的内容,确保了扩展的稳定,高效并且拥有较好的向后兼容性。

  值得一提的是,我在1/26号便基于ASP.NET MVC的Beta版本写出了这个扩展的第一个版本,而在不久之后微软发布了ASP.NET MVC RC。我在移植解决方案的过程中发现ASP.NET MVC RC在框架设计上进行了较大的改进,这使得我在构建扩展时的策略发生了些许变化。令人欣喜的是,RC版本的这些变化对于构建一个扩展,尤其是现在这种“低端”级别的扩展变得更加容易。ASP.NET MVC框架实现了它“到处可扩展”的承诺。

  那么我们现在就来详细分析一下这个扩展的实现方式。

  请求处理方式的改变
  
  在制定基本改造策略之前,我们需要了解ASP.NET MVC框架目前的架构及请求处理流程。如下:

  在应用程序启动时(此时还没有接受任何请求),将针对MVC请求的Route策略注册至ASP.NET Routing模块。此时每个Route策略(即Route对象)中的RouteHandler属性为ASP.NET MVC框架中的MvcRouteHandler。

  当ASP.NET Routing模块接收到一个匹配某个Route策略的HTTP请求时,将会调用该Route对象中RouteHandler对象的GetHttpHandler以获取一个HttpHandler,并交由ASP.NET执行。MvcRouteHandler永远将返回一个MvcHandler对象。

  MvcHandler在执行时,将取出RouteData中的controller值,并以此构建一个实现了IController接口的控制器对象,并调用IController接口的Execute方法执行该控制器。

  对于一个ASP.NET MVC应用程序来说,大部分控制器将会继承System.Web.Mvc.Controller类型。Controller类将会从RouteData获取action值,并交给实现IActionInvoker接口的对象来执行一个Action。
……
  
  如果我们要将这个流程改造成异步处理,那么就要让它符合ASP.NET架构中的异步处理方式。ASP.NET架构对于异步请求的处理可以体现在好几种方式上,例如异步页面,异步Http Module等,而最适合目前场合的做法自然是异步Http Handler。为实现一个异步Handler,我们需要让处理请求的Handler实现IHttpAsyncHandler接口,而不是传统的IHttpHandler接口。IHttpAsyncHandler接口中的BeginProcessRequest和EndProcessRequest两个方法构成了.NET中的APM(Aynchronous Programming Model,异步编程模型)模式,可以使用“二段式”的异步调用来处理一个HTTP请求。

  您应该已经发现,如果我们要支持异步Action,就必须根据当前的请求信息来确认究竟是执行一个IHttpHandler对象还是IHttpAsyncHandler对象。而在ASP.NET MVC框架在默认情况下是在Http Handler(即MvcHandler对象)内部进行控制器的检查,构造和调用。这为时已晚,我们必须讲这些逻辑提前到Routing过程中才行。幸运的是,ASP.NET Routing所支持的IRouteHandler就像是ASP.NET中的IHttpHandlerFactory,可以根据情况生成不同的Handler来执行。因此,我们只要构建一个新的IRouteHandler类型即可。于是就诞生了AsyncMvcRouteHandler——可以想象的出,其中的部分代码与框架中的MvcHandler相同,因为在一定程度上我们的确只是把原本在MvcHandler里做的事情给提前了:

public class AsyncMvcRouteHandler : IRouteHandler
{
    
public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        
string controllerName = requestContext.RouteData.GetRequiredString("controller");

        var factory
= ControllerBuilder.Current.GetControllerFactory();
        var controller
= factory.CreateController(requestContext, controllerName);
        
if (controller == null)
        {
            
throw new InvalidOperationException(...);
        }

        var coreController
= controller as Controller;
        
if (coreController == null)
        {
            
return new SyncMvcHandler(controller, factory, requestContext);
        }
        
else
        {

            
string actionName = requestContext.RouteData.GetRequiredString("action");
            
return IsAsyncAction(coreController, actionName, requestContext) ?
                (IHttpHandler)
new AsyncMvcHandler(coreController, factory, requestContext) :
                (IHttpHandler)
new SyncMvcHandler(controller, factory, requestContext);
        }
    }

    
internal static bool IsAsyncAction(
        Controller controller,
string actionName, RequestContext requestContext)
    {
        ...
    }
}

  在GetHttpHandler方法中,我们先从RouteData的controller字段中获取控制器的名字,并通过注册在ControllerBuilder上的Factory来创建一个实现了IController接口的控制器对象。由于我们需要使用Controller类中包含的ActionInvoker来辅助检测Action的异步需求,因此我们会设法将其转化为Controller类型。如果转换成功,就会取出RouteData中的action字段的值,并通过IsAsyncAction方法来确认当前Action是否应该异步执行。如果是,则返回一个实现了IHttpAsyncHandler的AsyncMvcHandler对象,否则就返回一个实现IHttpHandler的SyncMvcHandler对象。

   至于AsyncMvcRouteHandler的使用,只需在MapRoute时将Route Handler重新设置一下即可:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute(
"{resource}.axd/{*pathInfo}"
);

    routes.MapRoute(
        
"Default",                                              // Route name

        "{controller}/{action}/{id}",                           // URL with parameters
        new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
    ).RouteHandler = new AsyncMvcRouteHandler();
}

  检查是否为异步Action
  
  从上面的代码中我们已经形成了一个约定:如果要执行一个异步Action,那么控制器对象必须为Controller类型。这个约定的目的是为了使用Controller类中包含的IActionInvoker——确切地说,是ControllerActionInvoker类型里的功能。因此,另一个约定便是Controller的ActionInvoker对象必须返回一个ControllerActionInvoker的实例。

  ControllerActionInvoker中有一些辅助方法,能够返回对于一个Controller或Action的描述对象。从一个Action描述对象中我们可以获取关于这个Action的各种信息,而它是否被标记了AsyncActionAttribute,就是我们判断这个Action是否应该被异步执行的依据。如下:

private static object s_methodInvokerMutex = new object();
private static MethodInvoker s_controllerDescriptorGetter;

internal static bool IsAsyncAction(
    Controller controller,
string actionName, RequestContext requestContext)
{
    var actionInvoker
= controller.ActionInvoker as ControllerActionInvoker;
    
if (actionInvoker == null) return false;

    
if (s_controllerDescriptorGetter == null)
    {
        
lock (s_methodInvokerMutex)
        {
            
if (s_controllerDescriptorGetter == null)
            {
                BindingFlags bindingFlags
= BindingFlags.Instance | BindingFlags.NonPublic;
                MethodInfo method
= typeof(ControllerActionInvoker).GetMethod(
                    
"GetControllerDescriptor", bindingFlags);
                s_controllerDescriptorGetter
= new MethodInvoker(method);
            }
        }
    }

    var controllerContext
= new ControllerContext(requestContext, controller);
    var controllerDescriptor
= (ControllerDescriptor)s_controllerDescriptorGetter.Invoke(
        actionInvoker, controllerContext);
    var actionDescriptor
= controllerDescriptor.FindAction(controllerContext, actionName);
    
return actionDescriptor == null ? false :
        actionDescriptor.GetCustomAttributes(
typeof(AsyncActionAttribute), false).Any();
}

  ControllerActionInvoker类型中有个protected方法GetControllerDescriptor,它接受一个ControllerContext类型的参数,并返回一个ControllerDescriptor对象来描述当前控制器,而从该描述对象中可以通过FindAction方法获得一个ActionDescriptor对象来描述即将执行的Action。如果是一个不存在的Action,那么就返回false,最后就通过SyncMvcHandler对象来执行默认的行为。当且仅当该Action上拥有AsyncActionAttribute标记时,才说明它应该被异步执行,返回true。此外,这段代码中用到了MethodInvoker,这是一个辅助类,它来源于Fast Reflection Library,它实现了反射调用功能,但是它的性能十分接近于方法的直接调用,我在这篇文章中详细描述了这个项目的功能和使用。

  这段代码便涉及到ASP.NET MVC RC版本在Beta版本基础上的改进。在原先的ControllerActionInvoker类中只有获取Action方法的MethodInfo,而没有RC中各描述对象这样的抽象类型。从目前的设计上来看,我们使用的都是基于反射的抽象描述类型的子类。例如默认情况下,我们通过ActionDescriptor抽象类型访问的实际上是ReflectedActionDescriptor类型的实例。这是一个很有用的改进,由于我们通过描述对象进行抽象,于是我们就可以:

  使用不同的实现方式来描述各对象,默认情况下是使用基于反射(也就是“约定”)的实现,如果需要的话我们也可以使用基于配置文件的方式替换现有实现。

  使用特定对象的描述方式可以不拘泥于内部细节,例如一个异步的Action可能就由两个方法组成。
有了特定的描述对象,也方便添加额外的属性,例如该Action是否应该异步执行,是否应该禁用Session State等等。

  执行Action方法
  
  对于执行同步Action的SyncMvcHandler,其实现十分简单而直接:

public class SyncMvcHandler : IHttpHandler, IRequiresSessionState
{
    
public SyncMvcHandler(
        IController controller,
        IControllerFactory controllerFactory,
        RequestContext requestContext)
    {
        
this.Controller = controller;
        
this.ControllerFactory = controllerFactory;
        
this.RequestContext = requestContext;
    }

    
public IController Controller { get; private set; }
    
public RequestContext RequestContext { get; private set; }
    
public IControllerFactory ControllerFactory { get; private set; }

    
public virtual bool IsReusable { get { return false; } }

    
public virtual void ProcessRequest(HttpContext context)
    {
        
try
        {
            
this.Controller.Execute(this.RequestContext);
        }
        
finally
        {
            
this.ControllerFactory.ReleaseController(this.Controller);
        }
    }
}

  而对于异步Action,我之前一直思考着怎么将框架的默认实现,也就是单个方法调用,转化成两个方法(BeginXxx/EndXxx)调用。曾经我想过自己实现一个新的ActionInvoker,但是这就涉及到了大量的工作,尤其是如果希望保持框架现有的功能(ActionFilter,ActionSelector等等),最省力的方法可能就是继承ControllerActionInvoker,并设法使用框架已经实现的各种辅助方法。但是在分析了框架代码之后我发现复用也非常困难,举例来说,ControllerActionInvoker判定一个方法为Action的依据之一是这个方法返回的是ActionResult类型或其子类,这意味着我无法直接使用这个方法来获取一个返回IAsyncResult的BeginXxx方法;同理,对于查找EndXxx方法,我可能需要在请求名为Abc的异步Action时,将EndAbc作为查找依据交由现成的方法来查询——但是,如果又有一个请求是直接针对一个名为EndAbc的同步Action的那又怎么办呢?

  由于这些问题存在,我在去年设法实现异步Action时几乎重写了整个ActionInvoker——其复杂程度可见一斑。而且那个实现对于一些特殊情况的处理依旧不甚友好,需要开发人员在一定程度上做出妥协。这个实现在TechED 2008 China的Session中公布时我就承认它并不能让我满意,建议大家不要将其投入生产环境中。而现在的实现,则非常顺利地解决了整个问题。虽然从理论上讲还不够“完美”,虽然还做出了一些让步。
 
  带来如此多问题的原因就在于我们在设法颠覆框架内部的关键性设计,也就是从单一的Action方法调用,转变为“符合APM的”二段式调用。等等,您是否感觉到了解决问题的关键?没错,那就是“符合APM的”。APM要求我们将一个行为分为BeginXxx和EndXxx两个方法,可是既然ASP.NET MVC框架只能让我们返回一个ActionResult对象……那么我们为什么不在这个对象里包含方法的引用——也就是一个委托对象呢?这虽然不符合正统的APM签名,但是完全可行,不是吗?

public class AsyncActionResult : ActionResult
{
    
public AsyncActionResult(
        IAsyncResult asyncResult,
        Func
<IAsyncResult, ActionResult> endDelegate)
    {
        
this.AsyncResult = asyncResult;
        
this.EndDelegate = endDelegate;
    }

    
public IAsyncResult AsyncResult { get; private set; }

    
public Func<IAsyncResult, ActionResult> EndDelegate { get; private set; }

    
public override void ExecuteResult(ControllerContext context)
    {
        context.Controller
            .SetAsyncResult(
this.AsyncResult)
            .SetAsyncEndDelegate(
this.EndDelegate);
    }
}

  由于在Action方法中可以调用BeginXxx方法,我们在AsyncActionResult中只需保留Begin方法返回的IAsyncResult,以及另一个对于EndXxx方法的引用。在AsyncActionResult的ExecuteResult方法中将会保存这两个对象,以便在AsyncMvcHandler的EndProcessRequest方法中重新获取并使用。根据“惯例”,我们还需要定义一个扩展方法,方便开发人员在Action方法中返回一个AsyncActionResult。具体实现非常容易,在这里就展示一下异步Action的编写方式:

[AsyncAction]
public ActionResult AsyncAction(AsyncCallback asyncCallback, object asyncState)
{
    SqlConnection conn
= new SqlConnection("...;Asynchronous Processing=true");
    SqlCommand cmd
= new SqlCommand("WAITFOR DELAY '00:00:03';", conn);
    conn.Open();

    
return this.Async(
        cmd.BeginExecuteNonQuery(asyncCallback, asyncState),
        (ar)
=>
        {
            
int value = cmd.EndExecuteNonQuery(ar);
            conn.Close();
            
return this.View();
        });
}

  至此,似乎AsyncMvcHandler也无甚秘密可言了:

public class AsyncMvcHandler : IHttpAsyncHandler, IRequiresSessionState
{
    
public AsyncMvcHandler(
        Controller controller,
        IControllerFactory controllerFactory,
        RequestContext requestContext)
    {
        
this.Controller = controller;
        
this.ControllerFactory = controllerFactory;
        
this.RequestContext = requestContext;
    }

    
public Controller Controller { get; private set; }
    
public RequestContext RequestContext { get; private set; }
    
public IControllerFactory ControllerFactory { get; private set; }
    
public HttpContext Context { get; private set; }

    
public IAsyncResult BeginProcessRequest(
        HttpContext context,
        AsyncCallback cb,
        
object extraData)
    {
        
this.Context = context;
        
this.Controller.SetAsyncCallback(cb).SetAsyncState(extraData);

        
try
        {
            (
this.Controller as IController).Execute(this.RequestContext);
            
return this.Controller.GetAsyncResult();
        }
        
catch
        {
            
this.ControllerFactory.ReleaseController(this.Controller);
            
throw;
        }
    }

    
public void EndProcessRequest(IAsyncResult result)
    {
        
try
        {
            HttpContext.Current
= this.Context;
            ActionResult actionResult
= this.Controller.GetAsyncEndDelegate()(result);
            
if (actionResult != null)
            {
                actionResult.ExecuteResult(
this.Controller.ControllerContext);
            }
        }
        
finally
        {
            
this.ControllerFactory.ReleaseController(this.Controller);
        }
    }
}

  在BeginProcessRequest方法中将保存当前Context——这点很重要,HttpContext.Current是基于CallContext的,一旦经过一次异步回调HttpContext.Current就变成了null,我们必须重设。接着将接收到的AsyncCallback和AsyncState保留,并使用框架中现成的Execute方法执行控制器。当Execute方法返回时一整个Action方法的调用流程已经结束,这意味着其调用结果——即IAsyncResult和EndDelegate对象已经保留。于是将IAsyncResult对象取出并返回。至于EndProcessRequest方法,只是将BeginProcessRequest方法中保存下来的EndDelegate取出,调用,把得到的ActionResult再执行一遍即可。

  以上的代码只涉及到普通情况下的逻辑,而在完整的代码中还会包括对于Action方法被某个Filter终止或替换等特殊情况下的处理。此外,无论在BeginProcessRequest还是EndProcessRequest中都需要对异常进行合适地处理,使得Controller Factory能够及时地对Controller对象进行释放。

  ModelBinder支持
  
  其实您到目前为止还不能使用异步Action,因为您会发现方法的AsyncCallback参数得到的永远是null。这是因为默认的Model Binder无法得知如何从一个上下文环境中得到一个AsyncCallback对象。这一点倒非常简单,我们只需要构造一个AsyncCallbackModelBinder,而它的BindModel方法仅仅是将AsyncMvcHandler.BeginProcessRequest方法中保存的AsyncCallback对象取出并返回:

public sealed class AsyncCallbackModelBinder : IModelBinder
{
    
public object BindModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        
return controllerContext.Controller.GetAsyncCallback();
    }
}

  其使用方式,便是在应用程序启动时将其注册为AsyncCallback类型的默认Binder:

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
    ModelBinders.Binders[
typeof(AsyncCallback)] = new AsyncCallbackModelBinder();
}

  对于asyncState参数您也可以使用类似的做法,不过这似乎有些不妥,因为object类型实在过于宽泛,并不能明确代指asyncState参数。事实上,即使您不为asyncState设置binder也没有太大问题,因为对于一个异步ASP.NET请求来说,其asyncState永远是null。如果您一定要指定一个binder,我建议您在每个Action方法的asyncState参数上标记如下的Attribute,它和AsyncStateModelBinder也已经被一并建入项目中了:

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
public sealed class AsyncStateAttribute : CustomModelBinderAttribute
{
    
private static AsyncStateModelBinder s_modelBinder = new AsyncStateModelBinder();

    
public override IModelBinder GetBinder()
    {
        
return s_modelBinder;
    }
}

  使用方式如下:

[AsyncAction]
public ActionResult AsyncAction(AsyncCallback cb, [AsyncState]object state) { ... }

  其实,基于Controller的扩展方法GetAsyncCallback和GetAsyncState均为公有方法,您也可以让Action方法不接受这两个参数而直接从Controller中获取——当然这种做法降低了可测试性,不值得提倡。

  限制和缺点
  
  如果这个解决方案没有缺陷,那么相信它已经被放入ASP.NET MVC 1.0中,而轮不到我在这里扩展一番了。目前的这个解决方案至少有以下几点不足:

  没有严格遵守.NET中的APM模式,虽然不影响功能,但这始终是一个遗憾。

  由于利用了框架中的现成功能,所有的Filter只能运行在BeginXxx方法上。

  由于EndXxx方法和最终ActionResult的执行都没有Filter支持,因此如果在这个过程中抛出了异常,将无法进入ASP.NET MVC建议的异常处理功能中。
  
  根据ASP.NET MVC框架的Roadmap,ASP.NET MVC框架1.0之后的版本中将会支持异步Action,相信以上这些缺陷到时候都能被弥补。不过这就需要大量的工作,这只能交给ASP.NET MVC团队去慢慢执行了。事实上,您现在已经可以在ASP.NET MVC RC源代码的MvcFutures项目中找到异步Action处理的相关内容。它添加了IAsyncController,AsyncController,IAsyncActionInvoker,AsyncControllerActionInvoker等许多扩展。虽说它们都“继承”了现有的类,但是与我之前的判断相似,如AsyncControllerActionInvoker几乎完全重新实现了一遍ActionInvoker中的各种功能——我还没有仔细阅读代码,因此无法判断出这种设计是否优秀,只希望它能像ASP.NET MVC本身那样的简单和优雅。

  接下来,我打算为现在的代码的EndXxx方法也加上Filter支持,我需要仔细阅读ASP.NET MVC的源代码来寻找解决方案。希望它能够成为ASP.NET MVC正式支持异步Action之前较好的替代方案。

0
相关文章