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...
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:
GetContent()
call out of that event.GetContent()
, find the next place that your viewResult.ViewBag.SystemContent
is used and check for the task completion or wait on the completion.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>();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With