Extend ASP.NET MVC for Asynchronous Action
Numerous new features come into people’s eyes since ASP.NET 2.0 and asynchronous request handling is one of the most important parts. It reduces the working thread number by the async mechanism based on IO completion port when processing IO bound request and improves throughput of web sites significantly (please refer to the theory analyzing and benchmarks if you can read Chinese :P). But the current version of ASP.NET MVC doesn’t support async actions and that’s just ‘the missing key feature’ I mentioned before. I gave a so-called solution that introduced the async action to ASP.NET MVC in my session of TechED 2008 China but it’s full of limitations and inappropriate for production use.
These days I’m preparing for the session in the coming .NET conference in Shanghai (that Jeffrey Richter would also attend) and I think it’s better to release a better solution at that time: straightforward, rather full-featured and light-weighted – the core funtion contains only about 200 lines of codes, which means it maximize the usage of existing framework functions to make the solution more stable, efficient and backward-compatible.
I built the the new solution based on ASP.NET MVC Beta at first but RC released when I was writing the post. When I was trying to change the implementation for RC I found the improvements in design that impressed me a lot. These improvements change my strategy of building the solution but seems it’s a little easier when building extensions for RC. The ASP.NET MVC team did a good job.
Now let’s get started.
Change the way of request handling
It should be clear how ASP.NET MVC handles a request before we make decisions:
- When the application starts (without any incoming requests), route strategies would be registered in ASP.NET Routing module. At that time each strategy object (Route instance) contains a route handler – which is an intance of MvcRouteHandler type in ASP.NET MVC framework.
- When the Routing module meets a request that fit one of the strategies, the route handler belongs to the corresponding Route object would be used (by calling GetHttpHandler method) to get an http handler that can process the request. The MvcRouteHandler always returns an MvcHandler object.
- When MvcHandler object is processing the request, the ‘controller’ value in RouteData would be retrieved to build a controller object (which implements IController) by a controller factory (which implements IControllerFactory). After that, the controller would be executed (by calling Execute method on it).
- Generally, in an ASP.NET MVC application a controller type inherits from the System.Web.Mvc.Controller class. When executing these kind of controllers, the ‘action’ value in RouteData would be retrieved and passed to the action invoker (which is an IActionInvoker object get by the ActionInvoker property) to execute the action.
- …
If the way of processing requet needs to be async, we have to let it meet the architecture of ASP.NET. There’re several method to enabled the async mechanism such as async page and async http module, but the best one to use in our scenario is using async http handler. To implement an async http handler, we must make the handler type to implement IHttpAsyncHandler instead ot IHttpHandler. The BeginProcessRequest and EndProcessRequest methods compose the ‘two-phase’ processing style which meets the standard APM (Aynchronous Programming Model) pattern in .NET.
Now you may realized that an IHttpAsyncHandler should be used if we want to execute an action asynchronously, but the default choice of ASP.NET MVC framework is MvcHandler, which is always sync. It’s too late for us to decide whether the executing action is async or not in the handler. We have to move it to Routing. Fortunately, the route handler in ASP.NET Routing is just like IHttpHandlerFactory in ASP.NET architecture, which can be used to create an http handler object dynamiclly by context. So the first step we shoud do is to build a new route handler to replace the default MvcRouteHandler. Here comes the AsyncMvcRouteHandler – you can imagine that part of it is the same as MvcHandler since what we need to do is just to move some code to the earlier stage:
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) { ... } }
In GetHttpHandler method, we get the controller name from RouteData and create a controller object by the factory registered in ControllerBuilder. We try to cast the controller object as Controller type since we’ll use the action invoker in it to decide whether the action is async or not. If the controller object is in type Controller, we’ll get the action name from RouteData and try to return an AsyncMvcHandler (which implements IHttpAsyncHandler) or SyncMvcHandler (which implements IHttpHandler) object by the result of IsAsyncAction method.
To enable the AyncMvcRouteHandler, we should use it to replace the default one while mapping route strategies when application starts.
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(); }
Decide whether the action is async or not
We made a convention in code above: an async action could only be defined in an controller type which inherits from Controller. Here’s another one: the action invoker in the controller must be an object of ControllerActionInvoker or its child type.
There’re several ‘helper’ methods in ControllerActionInvoker which can be used to get the descriptors for controller and actions. We can get the action’s information from its descriptor object, such as whether it has been marked with AsyncActionAttribute – that’s the sign of an async 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(); }
There’s a protected method named ‘GetControllerDescriptor’. It accepts a ControllerContext object as parameter and returns a ControllerDescriptor object to describe the controller. From the descriptor of controller we can get the ActionDescriptor object as the descriptor of the executing action by calling FindAction method with the action name as one of the parameters. IsAsyncAction returns false for a non existing action so that SyncMvcHandler would be used to process the request with the default behavior. If and only if the action is marked with AsyncActionAttribute it would be identified as an async one. BTW, a special class ‘MethodInvoker’ is used above. It is a replacement for the function of MethodInfo.Invoke method and provide huge performance improvement. That’s a helper class from Fast Reflection Library project (I also wrote a post about it) that you can use for your own need.
Let’s talk about the change of design in ASP.NET MVC RC. There’s nothing like the descriptors but a method to get the MethodInfo of the action in previous ControllerActionInvoker. The current design uses the implementations based on reflection for the abstraction of descriptors by default. e.g., the descriptor of an action we used is an object of RelfectedActionDescriptor class, which implements the abstract class ActionDescriptor. That’s a great improvement. Since we’re using descriptors for elements (controller/action/parameter, etc.),
- we could use different concrete implementations to describe elements. e.g., we can build descriptors based on configurations rather than the default behavior – reflection – which is more like ‘convention’.
- there’s no limitations for the underly form of action. e.g., an async action could be composed by two methods.
- it’s easy to add more information on descriptor types. e.g., in the future we could know whether the action disabled session state from an action descriptor.
- …
Execute an action
The SyncMvcHandler type which used to execute a normal action is quite an easy one:
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); } } }
But I thought a lot for dealing with an async action, or how I can change the default execution style into ‘two-phase’ (BeginXxx/EndXxx). I tried to implement a new action invoker before but found it would take a lot of work. If I want to keep all the current features (action filter, action selector, etc.), it seems the best way is to build a child class of ControllerActionInvoker and use te exsiting functions as much as possible. But I think it’s nealy impossible after reading the code. For example, one of the characters of an action method is that it returns ActionResult (or its child), but the BeginXxx method for an async action returns IAsyncResult so we cannot use the FindAction method in ControllerActionInvoker directly; and, if we want to use FindAction to get the ‘EndAbc’ method by passing the action name ‘Abc’ with ‘End’ before it, what would happen if there’s a request to execute a sync action ‘EndAbc’?
Since the problems described above, I almost rewrite the whole invoker last year and it brings a lot of complexities and limitations. Developers have to compromise in some aspect when using it. When I introduced the solution in my session of TechED 2008 China I said it should not be used in production environment.
The current solution is much better. It uses an interesting work around to solve the problems. It’s not perfect but usable. The reason we meet such difficulties is that we break the design of framework, from single action method to a ‘APM-style’ execution. Wait a minute, have you idetified the source of the problems? That’s right, it’s the ‘APM-style’.
‘APM-style’ separates a single method into a pair of BeginXxx/EndXxx method, but we can just implement an async execute with an ‘two-phase’ style. Since the framework force an action to return an ActionResult, why can’t we keep the reference(s) of method(s) in the object? It’s not ‘APM-style’ but it’s really ‘async-style’, isn’t it?
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); } }
We execute the BeginXxx method in the action method and return an AsyncActionResult instance contains the IAsyncResut object and the reference to the EndXxx method. These two objects will be saved when the result executes and we can get them back in AsyncMvcHandler.EndProcessRequest method. Generally we should build an extension method to help the developers to return an AsyncActionResult object from an action method. Now we get the style of an async 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(); }); }
No secret for AsyncMvcHandler now:
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); } } }
We save the current HttpContext object in BeginProcessRequest – that’s important since HttpContext.Current is based on the call context and it would be thrown after an async callback, which means we shoud set it back in EndProcessRequest for the rest functions. After saving the HttpContext object, we should also save the async callback and async state parameters, and next, execute the controller. The whole progress is finished after Execute method’s return, which means the IAsyncResult object and the reference to the EndXxx method haved been saved. We get the IAsyncResult object back and return from BeginProcessRequest. In EndProcessRequest method, we retrive the saved reference to the EndXxx method and get another ActionResult by calling it. Finally we execute the new result and complete the whole async processing.
The code above only considers the logic of normal state. You can get more (e.g. logic to deal with the exceptional state) in the source code of the solution. Both the BeignProcessRequest and EndProcessRequest methods take care of error handling to make the controller released at a property time.
ModelBinder support
Actually you cannot use async action so far, since the default model binder don’t know how to bind an AsyncCallback object and the AsyncCallback parameter in the action method is always null. That’s easy. We could build a AsyncCallbackModelBinder whose only purpose is to return the AsyncCallback object saved in controller:
public sealed class AsyncCallbackModelBinder : IModelBinder { public object BindModel( ControllerContext controllerContext, ModelBindingContext bindingContext) { return controllerContext.Controller.GetAsyncCallback(); } }
We should register the model binder for AsyncCallback type when application starts.
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ModelBinders.Binders[typeof(AsyncCallback)] = new AsyncCallbackModelBinder(); }
It’s inappropriate to do so for async state parameter in an action method since object is the base class of all the types and it’s not specific to the async state. I suggest that you mark an attribute for the async state parameter in each action method. Here’s the AsyncStateAttribute, which has been built into the solution with 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; } }
Here’s the way to use it:
[AsyncAction] public ActionResult AsyncAction(AsyncCallback cb, [AsyncState]object state) { ... }
Actually it won’t be a big deal even if you ignore the async state parameter since it’s always useless when processing a request aynchronously. And, you can also get the async callback and async state object by the extensions methods (GetAsyncCallback and GetAsyncState) defined for Controller type, but it reduces the testability.
Limitations and drawbacks
The solution definitely has limitations and drawbacks:
- It doesn’t follow the standard APM pattern.
- Since the solution is based on the existing functions in framework, all the filters would be executed when BeginXxx method finished.
- Some features are missed since the filters cannot be applied to EndXxx method and the final action result.
ASP.NET MVC will introduce the ‘async controller’ in the future according to the official roadmap. Actually the MvcFuture project in the source of ASP.NET MVC RC has already contains the code about async features. You can found serveral new types such as IAsyncController, AsyncController, IAsyncActionInvoker, AsyncControllerActionInvoker, etc.. All types are inherited from exsiting type but as I predicted before, the AsyncControllerActionInvoker class rewrites almost all the code. I cannot tell the quality of design before reading it – hope it is as good as the current one.
I’m going do more for the drawbacks listed above. I hope it would become a solution good enough for production. The whole solution, source code and benchmark have been published in MSDN Code Gallery. Please feel free to contact me for any comments and suggestions.