9

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!

svick
  • 236,525
  • 50
  • 385
  • 514
Pete Field
  • 485
  • 4
  • 11
  • 1
    I'm not familiar with Umbraco, but the fact that it's converting `Task` 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 Apr 11 '14 at 11:20
  • 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 Apr 11 '14 at 11:36
  • I have posted a similar problem on http://stackoverflow.com/questions/30166566/umbraco-7-asp-net-mvc-async-controller-returning-system-threading-tasks-task1 using a RenderMvcController - any luck with this?? It's a year later but still the same problem! Am I missing something? – legas May 11 '15 at 23:05
  • 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](http://issues.umbraco.org/issue/U4-7270) and written about [a workaround here](http://www.digbyswift.com/blog/2015/10/asyncawait-in-umbraco-custom-controllers/). – Digbyswift Oct 17 '15 at 14:06

2 Answers2

10

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.

Pete Field
  • 485
  • 4
  • 11
  • 1
    Thanks Pete. Your answer helped me solved my own issue with this problem (though not with Umbraco). In MVC5, my `ControllerFactory` was hooking up an old `ActionInvoker` for hooking into ELMAH that was inheriting from `ControllerActionInvoker`. When I changed this to `AsyncControllerActionInvoker` the problem was resolved. – Hanshan May 21 '15 at 02:35
4

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);
    }
}
Shazwazza
  • 781
  • 5
  • 18