代码改变世界

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

2009-02-04 09:04 by Jeffrey Zhao, ... 阅读, ... 评论, 收藏, 编辑

本文分为上下两部分,您也可以从《Extend ASP.NET MVC for Asynchronous Action》获得全部内容。

执行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中,而轮不到我在这里扩展一番了。目前的这个解决方案至少有以下几点不足:

  1. 没有严格遵守.NET中的APM模式,虽然不影响功能,但这始终是一个遗憾。
  2. 由于利用了框架中的现成功能,所有的Filter只能运行在BeginXxx方法上。
  3. 由于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之前较好的替代方案。

更多资料

完整的项目代码已经放置在MSDN Code Gallery中,您可以在这里访问到关于它的“性能测试”等更多信息。这篇文章着重讲解了扩展的设计原理,省略了涉及特殊状况处理以及程序健壮性等实现细节的描述,欢迎您下载代码并提出改进建议。

 

上一篇:为ASP.NET MVC扩展异步Action功能(上)

使用Live Messenger联系我