Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async Controller Action with Umbraco 7 returns string

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!

like image 403
Pete Field Avatar asked Apr 11 '14 08:04

Pete Field


2 Answers

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.

like image 164
Pete Field Avatar answered Sep 18 '22 15:09

Pete Field


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);
    }
}
like image 4
Shazwazza Avatar answered Sep 20 '22 15:09

Shazwazza