為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 boolIsReusable { 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之前較好的替代方案。
更多資料
完整的專案程式碼已經放置在MSDN Code Gallery中,您可以在這裡訪問到關於它的“效能測試”等更多資訊。這篇文章著重講解了擴充套件的設計原理,省略了涉及特殊狀況處理以及程式健壯性等實現細節的描述,歡迎您下載程式碼並提出改進建議。