為ASP.NET MVC擴展異步Action功能(下)
編輯推薦:為ASP.NET MVC擴展異步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對象進行釋放。
#p#
ModelBinder支持
其實您到目前為止還不能使用異步Action,因為您會發現方法的AsyncCallback參數得到的永遠是null。這是因為默認的Model Binder無法得知如何從一個上下文環境中得到一個AsyncCallback對象。這一點倒非常簡單,我們只需要構造一個AsyncCallbackModelBinder,而它的BindModel方法僅僅是將AsyncMvcHandler.BeginProcessRequest方法中保存的AsyncCallback對象取出并返回:
public sealed class AsyncCallbackModelBinder : IModelBinder |
其使用方式,便是在應用程序啟動時將其注冊為AsyncCallback類型的默認Binder:
protected void Application_Start() |
對于asyncState參數您也可以使用類似的做法,不過這似乎有些不妥,因為object類型實在過于寬泛,并不能明確代指asyncState參數。事實上,即使您不為asyncState設置binder也沒有太大問題,因為對于一個異步ASP.NET請求來說,其asyncState永遠是null。如果您一定要指定一個binder,我建議您在每個Action方法的asyncState參數上標記如下的Attribute,它和AsyncStateModelBinder也已經被一并建入項目中了:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] public override IModelBinder GetBinder() |
使用方式如下:
[AsyncAction] |
其實,基于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本身那樣的簡單和優雅。
【編輯推薦】