Sunday, November 20, 2011

Running Asp.Net MVC controller actions on STA threads

Recently while we were converting a legacy asp application (Yes they still exist) to asp.net mvc, we had to work with a set of 3rd party business critical components.

These components were legacy COM components.  In load testing we found that controller actions that contain calls to these components were crashing w3wp process almost every minute.   A little bit of research around this problem yielded the following article.

Running ASMX Web Services on STA Threads

The summary of the problem is, MVC action methods run in COM multithreaded apartment (MTA) threads.  These legacy components were being created from MTA threads are being serialized and are being processed by single STA thread.  On top of that these components are loading tons of data in to memory and the load is causing the memory corruption.

So the solution is to make the MVC action method to run on STA thread, thus allowing COM to place the object instances on the creator’s thread.

Here is an MSDN thread that ties up the STA with asp.net mvc

AspCompat=true does not work with MVC

The above solution doesn’t support asp.net sessions and also was written for asp.net mvc 1.0

Here is the asp.net mvc 3.0 adjusted solution.

First the RouteHandler, it derives from MVCRouteHandler and instantiate a IHttpHandler derived class.

public class STARouteHandler : MvcRouteHandler
{
   protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
   {
       return new STARequestHandler(requestContext);
   }
}


Next the STARequestHandler,  the key to this entire magic to work is in the BeginProcessRequest and EndProcessRequest methods. These two methods create an aspnet compat wrapper around the actual execution of action method.  This handler also implements IRequiresSessionState marker interface to support the sessions.


public class STARequestHandler : Page, IHttpAsyncHandler, IRequiresSessionState
{
    public STARequestHandler(RequestContext requestContext)
    {
        if (requestContext == null)
            throw new ArgumentNullException("requestContext");
        this.RequestContext = requestContext;
    }

    private ControllerBuilder _controllerBuilder;

    internal ControllerBuilder ControllerBuilder
    {
        get { return this._controllerBuilder ?? (this._controllerBuilder = ControllerBuilder.Current);}
    }

    public RequestContext RequestContext { get; set; }

    protected override void OnInit(EventArgs e)
    {
        string requiredString = this.RequestContext.RouteData.GetRequiredString("controller");
        var controllerFactory = this.ControllerBuilder.GetControllerFactory();
        var controller = controllerFactory.CreateController(this.RequestContext, requiredString);
        if (controller == null)
            throw new InvalidOperationException("Could not find controller: " + requiredString);
        try
        {
            controller.Execute(this.RequestContext);
        }
        finally
        {
            controllerFactory.ReleaseController(controller);
        }
        this.Context.ApplicationInstance.CompleteRequest();
    }

    public override void ProcessRequest(HttpContext httpContext)
    {
        throw new NotSupportedException("This should not get called for an STA");
    }

    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
    {
        return this.AspCompatBeginProcessRequest(context, cb, extraData);
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        this.AspCompatEndProcessRequest(result);
    }

    void IHttpHandler.ProcessRequest(HttpContext httpContext)
    {
        this.ProcessRequest(httpContext);
    }
}

 And lastly the usage of this handler.

While creating a route for the action method, simply attach this handler to the route definition.


context.MapRoute("STARoute", "{controller/{action}",
                    new { controller = "Home", action = "Index")
                    .RouteHandler = new STARouteHandler();


You can test the apartment state by calling the following line of code in the action method.

Thread.CurrentThread.ApartmentState.ToString();

No comments:

Post a Comment