6

Since MVC 4 does not support asynchronous child actions (via Html.Action) I'm looking for a way to force the synchronous execution child actions. A simple workaround to this limitation is to provide synchronous versions of all my controllers actions:

public class FooAsyncController : Controller {
    public async Task<ActionResult> IndexAsync() {
        var model = await service.GetFoo().ConfigureAwait(false);
        return View(model);
    }   
}

public class FooSyncController : FooAsyncController {
    public ActionResult Index() {
        return IndexAsync().Result; // blocking call
    }   
}

However, since we allow child action requests on all of our controller actions, doing this for every controller is a real PITA.

Are there any extensibility points in the framework where we could inspect the return value of an action and, if it returns a Task<T> and we are handling a child action, force a synchronous call?

3
  • 1
    The first thing that comes to mind is to make a new extension method Html.ActionAsync that will simply invoke the pipeline like you want, but add the .Result call to the end of the helper method body, thus turning your async calls into sync calls. Interesting problem, for sure. Commented Oct 31, 2012 at 19:39
  • 1
    Were this me, I'd create a new controller that overrides CreateActionInvoker or HandleUnknownAction and with reflection look at a target controller to do a synchronous invocation. You'd only have to implement the controller once and can reuse it for any async action. Commented Oct 31, 2012 at 19:48
  • @Tejs yes this would be the idea solution although this area of the framework is not very async friendly. Problem seems to be somewhere deep inside HttpServerUtilityBase.Execute. @ThikingSites - why not post an answer with more details? this sounds like a good workaround. Commented Oct 31, 2012 at 23:11

1 Answer 1

3

Having trawled through the ASP.NET MVC source code for hours the best solution I've been able to come up with (aside from creating synchronous versions of every controller action) is to manually invoke the action descriptor for the async Action methods within Controller.HandleUnknownAction.

I'm not particularly happy with this code and I hope that it can be improved, but it does work.

The idea is to purposely request an invalid action (prefixed with "_") which will invoke the HandleUnknownAction method on the controller. Here we look for a matching async action (by first removing the underscore from the actionName) and invoke the AsyncActionDescriptor.BeginExecute method. By immediately calling the EndExecute method we are effectively executing the action descriptor synchronously.

public ActionResult Index()
{
    return View();
}


public async Task<ActionResult> Widget(int page = 10)
{
    var content = await new HttpClient().GetStringAsync("http://www.foo.com")
        .ConfigureAwait(false);
    ViewBag.Page = page;
    return View(model: content);
}

protected override void HandleUnknownAction(string actionName)
{
    if (actionName.StartsWith("_"))
    {
        var asyncActionName = actionName.Substring(1, actionName.Length - 1);
        RouteData.Values["action"] = asyncActionName;

        var controllerDescriptor = new ReflectedAsyncControllerDescriptor(this.GetType());
        var actionDescriptor = controllerDescriptor.FindAction(ControllerContext, asyncActionName)
            as AsyncActionDescriptor;

        if (actionDescriptor != null)
        {
            AsyncCallback endDelegate = delegate(IAsyncResult asyncResult)
            {

            };

            IAsyncResult ar = actionDescriptor.BeginExecute(ControllerContext, RouteData.Values, endDelegate, null);
            var actionResult = actionDescriptor.EndExecute(ar) as ActionResult;

            if (actionResult != null)
            {
                actionResult.ExecuteResult(ControllerContext);
            }
        }
    }
    else
    {
        base.HandleUnknownAction(actionName);
    }
}

The view

<h2>Index</h2>

@Html.Action("_widget", new { page = 5 }) <!-- note the underscore prefix -->

I'm almost certain there is a better way by overriding Controller.BeginExecute. The default implementation can be seen below. The idea would be to execute Controller.EndExecuteCore immediately although I've not had any success with this so far.

protected virtual IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state)
{
    if (DisableAsyncSupport)
    {
        // For backwards compat, we can disallow async support and just chain to the sync Execute() function.
        Action action = () =>
        {
            Execute(requestContext);
        };

        return AsyncResultWrapper.BeginSynchronous(callback, state, action, _executeTag);
    }
    else
    {
        if (requestContext == null)
        {
            throw new ArgumentNullException("requestContext");
        }

        // Support Asynchronous behavior. 
        // Execute/ExecuteCore are no longer called.

        VerifyExecuteCalledOnce();
        Initialize(requestContext);
        return AsyncResultWrapper.Begin(callback, state, BeginExecuteCore, EndExecuteCore, _executeTag);
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.