9
votes

Is it possible to use an async action within an Umbraco SurfaceController (and UmbracoApiController)

I tried the following code

public async Task< ActionResult> HandleLogin(LoginViewModel model)
{
    await Task.Delay(1000);
    return PartialView("Login", model);
}

and although it compiled correctly when the action is called the action seems to return as soon as the await is hit, and returns a string

System.Threading.Tasks.Task`1[System.Web.Mvc.ActionResult]

the controller of course inherits from SurfaceController and I wonder if this is the problem?

If this is not possible, are there any workarounds to achieve async action behaviour?

Any help would be gratefully received!

2
I'm not familiar with Umbraco, but the fact that it's converting Task<ActionResult> to a string indicates that it does not understand async methods. You might need to contact the Umbraco community directly, and/or put in a feature request.Stephen Cleary
Thanks, yeah I thought it might be somethign like that. Have asked a question on our.umbraco too. Will update here if any feedback!Pete Field
I have posted a similar problem on stackoverflow.com/questions/30166566/… using a RenderMvcController - any luck with this?? It's a year later but still the same problem! Am I missing something?legas
This issue has been fixed for controllers that inherit from Umbraco.Web.Mvc.SurfaceController but this is still an issue for controllers that implement IRenderMvcController. I've raised it as an issue here and written about a workaround here.Digbyswift

2 Answers

10
votes

The SurfaceControllers in Umbraco ultimately derive from System.Web.Mvc.Controller However they have custom action invoker (RenderActionInvoker) set.

RenderActionInvoker inherits from ContollerActionInvoker. In order to process async actions it should instead derive from AsyncContolkerActionInvoker. RenderActionInvoker overrides only the findaction method so changing to derive from AsyncContolkerActionInvoker is easy.

Once I recompiled Umbraco.Web with this change, async actions worked fine.

Rather than recompiling the whole project, I guess you could specify a new actioninvoker on each class

public class RenderActionInvokerAsync : System.Web.Mvc.Async.AsyncControllerActionInvoker
{

    protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
    {
        var ad = base.FindAction(controllerContext, controllerDescriptor, actionName);

        if (ad == null)
        {
            //check if the controller is an instance of IRenderMvcController
            if (controllerContext.Controller is IRenderMvcController)
            {
                return new ReflectedActionDescriptor(
                    controllerContext.Controller.GetType().GetMethods()
                        .First(x => x.Name == "Index" &&
                                    x.GetCustomAttributes(typeof(NonActionAttribute), false).Any() == false),
                    "Index",
                    controllerDescriptor);

            }
        }
        return ad;
    }

}

public class TestController : SurfaceController
{

    public TestController() {
        this.ActionInvoker = new RenderActionInvokerAsync();
    }

    public async Task<ActionResult> Test()
    {
        await Task.Delay(10000);
        return PartialView("TestPartial");

    }
}

Haven't tested this way of doing things though.

4
votes

Just FYI I've added an issue to the tracker for this: http://issues.umbraco.org/issue/U4-5208

There is a work around though:

Create a custom async render action invoke (as per above):

public class FixedAsyncRenderActionInvoker : System.Web.Mvc.Async.AsyncControllerActionInvoker
{
    protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
    {
        var ad = base.FindAction(controllerContext, controllerDescriptor, actionName);

        if (ad == null)
        {
            //check if the controller is an instance of IRenderMvcController
            if (controllerContext.Controller is IRenderMvcController)
            {
                return new ReflectedActionDescriptor(
                    controllerContext.Controller.GetType().GetMethods()
                        .First(x => x.Name == "Index" &&
                                    x.GetCustomAttributes(typeof(NonActionAttribute), false).Any() == false),
                    "Index",
                    controllerDescriptor);

            }
        }
        return ad;
    }

}

Create a custom render mvc controller:

public class FixedAsyncRenderMvcController : RenderMvcController
{
    public FixedAsyncRenderMvcController()
    {
        this.ActionInvoker = new FixedAsyncRenderActionInvoker();
    }
}

Create a custom render controller factory:

public class FixedAsyncRenderControllerFactory : RenderControllerFactory
{
    public override IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controller1 = base.CreateController(requestContext, controllerName);
        var controller2 = controller1 as Controller;
        if (controller2 != null)
            controller2.ActionInvoker = new FixedAsyncRenderActionInvoker();
        return controller1;
    }
}

Create an umbraco startup handler and replace the necessary parts with the above custom parts:

public class UmbracoStartupHandler : ApplicationEventHandler
{
    protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
    {
        DefaultRenderMvcControllerResolver.Current.SetDefaultControllerType(typeof(FixedAsyncRenderMvcController));

        FilteredControllerFactoriesResolver.Current.RemoveType<RenderControllerFactory>();
        FilteredControllerFactoriesResolver.Current.AddType<FixedAsyncRenderControllerFactory>();

        base.ApplicationStarting(umbracoApplication, applicationContext);
    }
}