Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Awaited Async method not returning before result is required

Tags:

c#

async-await

I'm having trouble getting my head round an issue I'm having with async / await.

in a nutshell, I have a controller, decorated with an attribute. This attribute gets the specified content from an i/o intensive process (filesystem / db / api etc...)

It then sets the returned content as a Dictionary<string, string> on the ViewBag

Then, in a view, I can do something like this, to retrieve the content:

@(ViewBag.SystemContent["Common/Footer"])

The issue I'm having, is the first time it runs, the content hasn't returned, and the call to retrieve the value by string index fails, as it doesn't exist.
Hit F5, and it's fine.

Controller action is pretty simple:

[ProvideContent("Common/Footer")]
public class ContactDetailsController : Controller
{
    public async Task<ActionResult> Index()
    {
        //omitted for brevity - awaits some other async methods
        return View();
    }
}

The attribute

public override async void OnActionExecuting(ActionExecutingContext filterContext)
{
    if (filterContext.Result is ViewResult)
    {
        var localPath = filterContext.RouteData.Values["controller"] + "/" + filterContext.RouteData.Values["action"];

        if (!_useControllerActionAsPath)
            localPath = _path;

        var viewResult = filterContext.Result as ViewResult;

        //this seems to go off and come back AFTER the view requests it from the dictionary
        var content = await _contentManager.GetContent(localPath);

        if (viewResult.ViewBag.SystemContent == null)
            viewResult.ViewBag.SystemContent = new Dictionary<string, MvcHtmlString>();

        viewResult.ViewBag.SystemContent[localPath] = new DisplayContentItem(content, localPath);
    }

EDIT

Changing the following line in my Attribute:

var content = await _contentManager.GetContent(localPath);

To

var content = Task.Factory.StartNew(() =>
            manager.GetContent(localPath).Result, TaskCreationOptions.LongRunning).Result;

solves the problem, but I feel it goes against everything I've read on Stephen Clearys blog...

like image 610
Alex Avatar asked Apr 08 '15 15:04

Alex


1 Answers

I'm not 100% familiar with the ASP.Net MVC stack and how all this works, but I'm going to take a stab at it.

The OnActionExecuting() documentation says:

Called before the action method is invoked.

Since you overrode a previously synchronous method and made it asynchronous, it's expecting that code to be complete, and ready for the next execution step.

Theoretical execution path:

public void ExecuteAction()
{
 OnActionExecuting();

 OnActionExecution();

 OnActionExecuted();
}

Since you overrode the OnActionExecuting() method, the execution stack is basically still the same, but the next code to be executed (ExecuteAction() and OnActionExecuted() and whatever called ExecuteAction()) were expecting an synchronous calls to be made, so to their knowledge everything is fine and ready to keep on running.

Basically, this boils down to OnActionExecuting() isn't an asynchronous method, and nothing is expecting it to be. (It's not asynchronous in MVC 6 either.)

Something, that's being called synchronously after OnActionExecuting() and it's sequential calls, is referencing viewResult.ViewBag.SystemContent and thus it's not getting the value you're wanting. As you said yourself in the title,

Awaited Async method not returning before result is required.

The 'catch' with using Tasks is that you can't guarantee when the task will complete, but you are guaranteed that it will complete.

Potential solutions:

  • Move the GetContent() call out of that event.
  • Store off the Task that's created for GetContent(), find the next place that your viewResult.ViewBag.SystemContent is used and check for the task completion or wait on the completion.
  • Add a timeout interval to the GetContent() method. (Tons of different ways to do this. MSDN Docs for Task class This won't fix your problem.

Edit: Code sample for storing Task in controller

[ProvideContent("Common/Footer")]
public class ContactDetailsController : Controller
{

/*
 * BEGINNING OF REQUIRED CODE BLOCK
 */
    private Task<string> _getContentForLocalPathTask; 
    private string _localPath;

/*
 * END OF REQUIRED CODE BLOCK
 */
    public async Task<ActionResult> Index()
    {
        //omitted for brevity - awaits some other async methods
/*
 * BEGINNING OF REQUIRED CODE BLOCK
 */
        string content = await _getContentForLocalPath;

        viewResult.ViewBag.SystemContent[_localPath] = new DisplayContentItem(content, _localPath);            
/*
 * END OF REQUIRED CODE BLOCK
 */

         return View();
    }

public override async void OnActionExecuting(ActionExecutingContext filterContext)
{
    if (filterContext.Result is ViewResult)
    {
        var localPath = filterContext.RouteData.Values["controller"] +
                         "/" + filterContext.RouteData.Values["action"];

        if (!_useControllerActionAsPath)
            localPath = _path;

        var viewResult = filterContext.Result as ViewResult;

/*
 * BEGINNING OF REQUIRED CODE BLOCK
 */
       _localPath = localPath;

       _getContentForLocalPathTask = _contentManager.GetContent(localPath);
/*
 * END OF REQUIRED CODE BLOCK
 */

        if (viewResult.ViewBag.SystemContent == null)
            viewResult.ViewBag.SystemContent = new Dictionary<string, MvcHtmlString>();

    }
}
like image 50
Cameron Avatar answered Oct 02 '22 15:10

Cameron