Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Force synchronous execution of asynchronous action in ASP.NET MVC 4

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?

like image 437
Ben Foster Avatar asked Oct 31 '12 19:10

Ben Foster


People also ask

How to implement asynchronous methods in MVC?

Implementing Asynchronous Methods in Asp.Net MVC. The first thing to do is to add the async keyword to Action Method. If we use the async Keyword in Method, the Method must also use await Keyword. The return type of an async method must be void, Task or Task<T> we have used Task<T> in Action Method.

What is asynchronous in ASP NET 4?

ASP.NET 4.5 Web Pages in combination .NET 4.5 enables you to register asynchronous methods that return an object of type Task. The .NET Framework 4 introduced an asynchronous programming concept referred to as a Task and ASP.NET 4.5 supports Task.

What is the difference between asynchronous and synchronous action methods?

Asynchronous action methods have a significant advantage over synchronous methods when an action must perform several independent operations. In the sample provided, the synchronous method PWG (for Products, Widgets and Gizmos) displays the results of three web service calls to get a list of products, widgets, and gizmos.

What is async wait and task in MVC?

In .Net 4.5 Framework Microsoft has introduced 3 new keywords async, await, and Task. The Task is a class that comes under the namespace System.Threading.Tasks. Now we had lots of theory, let’s do some Practical. Here we will learn how to use async wait and Task in asp.net mvc project and how it is different from traditional methods (Synchronous).


1 Answers

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);
    }
}
like image 123
Ben Foster Avatar answered Dec 17 '22 00:12

Ben Foster