為ASP.NET MVC擴(kuò)展異步Action功能(下)
編輯推薦:為ASP.NET MVC擴(kuò)展異步Action功能(上)
執(zhí)行Action方法
對(duì)于執(zhí)行同步Action的SyncMvcHandler,其實(shí)現(xiàn)十分簡(jiǎn)單而直接:
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);
}
}
}
而對(duì)于異步Action,我之前一直思考著怎么將框架的默認(rèn)實(shí)現(xiàn),也就是單個(gè)方法調(diào)用,轉(zhuǎn)化成兩個(gè)方法(BeginXxx/EndXxx)調(diào)用。曾經(jīng)我想過(guò)自己實(shí)現(xiàn)一個(gè)新的ActionInvoker,但是這就涉及到了大量的工作,尤其是如果希望保持框架現(xiàn)有的功能(ActionFilter,ActionSelector等等),最省力的方法可能就是繼承ControllerActionInvoker,并設(shè)法使用框架已經(jīng)實(shí)現(xiàn)的各種輔助方法。但是在分析了框架代碼之后我發(fā)現(xiàn)復(fù)用也非常困難,舉例來(lái)說(shuō),ControllerActionInvoker判定一個(gè)方法為Action的依據(jù)之一是這個(gè)方法返回的是ActionResult類型或其子類,這意味著我無(wú)法直接使用這個(gè)方法來(lái)獲取一個(gè)返回IAsyncResult的BeginXxx方法;同理,對(duì)于查找EndXxx方法,我可能需要在請(qǐng)求名為Abc的異步Action時(shí),將EndAbc作為查找依據(jù)交由現(xiàn)成的方法來(lái)查詢——但是,如果又有一個(gè)請(qǐng)求是直接針對(duì)一個(gè)名為EndAbc的同步Action的那又怎么辦呢?
由于這些問(wèn)題存在,我在去年設(shè)法實(shí)現(xiàn)異步Action時(shí)幾乎重寫了整個(gè)ActionInvoker——其復(fù)雜程度可見(jiàn)一斑。而且那個(gè)實(shí)現(xiàn)對(duì)于一些特殊情況的處理依舊不甚友好,需要開發(fā)人員在一定程度上做出妥協(xié)。這個(gè)實(shí)現(xiàn)在TechED 2008 China的Session中公布時(shí)我就承認(rèn)它并不能讓我滿意,建議大家不要將其投入生產(chǎn)環(huán)境中。而現(xiàn)在的實(shí)現(xiàn),則非常順利地解決了整個(gè)問(wèn)題。雖然從理論上講還不夠“完美”,雖然還做出了一些讓步。
帶來(lái)如此多問(wèn)題的原因就在于我們?cè)谠O(shè)法顛覆框架內(nèi)部的關(guān)鍵性設(shè)計(jì),也就是從單一的Action方法調(diào)用,轉(zhuǎn)變?yōu)椤胺螦PM的”二段式調(diào)用。等等,您是否感覺(jué)到了解決問(wèn)題的關(guān)鍵?沒(méi)錯(cuò),那就是“符合APM的”。APM要求我們將一個(gè)行為分為BeginXxx和EndXxx兩個(gè)方法,可是既然ASP.NET MVC框架只能讓我們返回一個(gè)ActionResult對(duì)象……那么我們?yōu)槭裁床辉谶@個(gè)對(duì)象里包含方法的引用——也就是一個(gè)委托對(duì)象呢?這雖然不符合正統(tǒng)的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方法中可以調(diào)用BeginXxx方法,我們?cè)贏syncActionResult中只需保留Begin方法返回的IAsyncResult,以及另一個(gè)對(duì)于EndXxx方法的引用。在AsyncActionResult的ExecuteResult方法中將會(huì)保存這兩個(gè)對(duì)象,以便在AsyncMvcHandler的EndProcessRequest方法中重新獲取并使用。根據(jù)“慣例”,我們還需要定義一個(gè)擴(kuò)展方法,方便開發(fā)人員在Action方法中返回一個(gè)AsyncActionResult。具體實(shí)現(xiàn)非常容易,在這里就展示一下異步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也無(wú)甚秘密可言了:
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方法中將保存當(dāng)前Context——這點(diǎn)很重要,HttpContext.Current是基于CallContext的,一旦經(jīng)過(guò)一次異步回調(diào)HttpContext.Current就變成了null,我們必須重設(shè)。接著將接收到的AsyncCallback和AsyncState保留,并使用框架中現(xiàn)成的Execute方法執(zhí)行控制器。當(dāng)Execute方法返回時(shí)一整個(gè)Action方法的調(diào)用流程已經(jīng)結(jié)束,這意味著其調(diào)用結(jié)果——即IAsyncResult和EndDelegate對(duì)象已經(jīng)保留。于是將IAsyncResult對(duì)象取出并返回。至于EndProcessRequest方法,只是將BeginProcessRequest方法中保存下來(lái)的EndDelegate取出,調(diào)用,把得到的ActionResult再執(zhí)行一遍即可。
以上的代碼只涉及到普通情況下的邏輯,而在完整的代碼中還會(huì)包括對(duì)于Action方法被某個(gè)Filter終止或替換等特殊情況下的處理。此外,無(wú)論在BeginProcessRequest還是EndProcessRequest中都需要對(duì)異常進(jìn)行合適地處理,使得Controller Factory能夠及時(shí)地對(duì)Controller對(duì)象進(jìn)行釋放。
#p#
ModelBinder支持
其實(shí)您到目前為止還不能使用異步Action,因?yàn)槟鷷?huì)發(fā)現(xiàn)方法的AsyncCallback參數(shù)得到的永遠(yuǎn)是null。這是因?yàn)槟J(rèn)的Model Binder無(wú)法得知如何從一個(gè)上下文環(huán)境中得到一個(gè)AsyncCallback對(duì)象。這一點(diǎn)倒非常簡(jiǎn)單,我們只需要構(gòu)造一個(gè)AsyncCallbackModelBinder,而它的BindModel方法僅僅是將AsyncMvcHandler.BeginProcessRequest方法中保存的AsyncCallback對(duì)象取出并返回:
public sealed class AsyncCallbackModelBinder : IModelBinder |
其使用方式,便是在應(yīng)用程序啟動(dòng)時(shí)將其注冊(cè)為AsyncCallback類型的默認(rèn)Binder:
protected void Application_Start() |
對(duì)于asyncState參數(shù)您也可以使用類似的做法,不過(guò)這似乎有些不妥,因?yàn)閛bject類型實(shí)在過(guò)于寬泛,并不能明確代指asyncState參數(shù)。事實(shí)上,即使您不為asyncState設(shè)置binder也沒(méi)有太大問(wèn)題,因?yàn)閷?duì)于一個(gè)異步ASP.NET請(qǐng)求來(lái)說(shuō),其asyncState永遠(yuǎn)是null。如果您一定要指定一個(gè)binder,我建議您在每個(gè)Action方法的asyncState參數(shù)上標(biāo)記如下的Attribute,它和AsyncStateModelBinder也已經(jīng)被一并建入項(xiàng)目中了:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] public override IModelBinder GetBinder() |
使用方式如下:
[AsyncAction] |
其實(shí),基于Controller的擴(kuò)展方法GetAsyncCallback和GetAsyncState均為公有方法,您也可以讓Action方法不接受這兩個(gè)參數(shù)而直接從Controller中獲取——當(dāng)然這種做法降低了可測(cè)試性,不值得提倡。
限制和缺點(diǎn)
如果這個(gè)解決方案沒(méi)有缺陷,那么相信它已經(jīng)被放入ASP.NET MVC 1.0中,而輪不到我在這里擴(kuò)展一番了。目前的這個(gè)解決方案至少有以下幾點(diǎn)不足:
1. 沒(méi)有嚴(yán)格遵守.NET中的APM模式,雖然不影響功能,但這始終是一個(gè)遺憾。
2. 由于利用了框架中的現(xiàn)成功能,所有的Filter只能運(yùn)行在BeginXxx方法上。
3. 由于EndXxx方法和最終ActionResult的執(zhí)行都沒(méi)有Filter支持,因此如果在這個(gè)過(guò)程中拋出了異常,將無(wú)法進(jìn)入ASP.NET MVC建議的異常處理功能中。
根據(jù)ASP.NET MVC框架的Roadmap,ASP.NET MVC框架1.0之后的版本中將會(huì)支持異步Action,相信以上這些缺陷到時(shí)候都能被彌補(bǔ)。不過(guò)這就需要大量的工作,這只能交給ASP.NET MVC團(tuán)隊(duì)去慢慢執(zhí)行了。事實(shí)上,您現(xiàn)在已經(jīng)可以在ASP.NET MVC RC源代碼的MvcFutures項(xiàng)目中找到異步Action處理的相關(guān)內(nèi)容。它添加了IAsyncController,AsyncController,IAsyncActionInvoker,AsyncControllerActionInvoker等許多擴(kuò)展。雖說(shuō)它們都“繼承”了現(xiàn)有的類,但是與我之前的判斷相似,如AsyncControllerActionInvoker幾乎完全重新實(shí)現(xiàn)了一遍ActionInvoker中的各種功能——我還沒(méi)有仔細(xì)閱讀代碼,因此無(wú)法判斷出這種設(shè)計(jì)是否優(yōu)秀,只希望它能像ASP.NET MVC本身那樣的簡(jiǎn)單和優(yōu)雅。
【編輯推薦】