This is a weird one I encountered in some legacy code that I've ear-marked for re-factor. I've checked it over and the code is not using .ConfigureAwait(false)..
The code in question looks much like this: (The "testx = ...." lines are part of debugging the issue to expose the behaviour.)
public async Task<ActionResult> Index()
{
ValidateRoleAccess(Roles.Admin, Roles.AuthorizedUser, Roles.AuditReadOnly);
var test1 = System.Web.HttpContext.Current != null
var decisions = await _lookupService.GetAllDecisions();
var test2 = System.Web.HttpContext.Current != null
var statuses = await _lookupService.GetAllEnquiryStatuses();
var test3 = System.Web.HttpContext.Current != null
var eeoGroups = await _lookupService.GetEEOGroups();
var test4 = System.Web.HttpContext.Current != null
var subCategories = await _lookupService.GetEnquiryTypeSubCategories();
var test5 = System.Web.HttpContext.Current != null
var paystreams = await _lookupService.GetPaystreams();
var test6 = System.Web.HttpContext.Current != null
var hhses = await _lookupService.GetAllHHS();
var test7 = System.Web.HttpContext.Current != null
// ...
The calls themselves are simple queries against EF through the same lookup service. Given that they are EF and use the same UoW / DbContext, these cannot be changed to use Task.WhenAll().
Expected Results: True for test1 -> test7
Actual Results: True for test1, -> test3, False for test4 -> test7
The issue was discovered when I added a validation against a particular role after the awaited lookup calls. The check tripped a null reference exception on HttpContext.Current which the validation method uses. So it passed when used in the ValidateRoleAccess call before the async, but failed if called after all the awaited methods.
I varied the order of the methods and it failed after either 2 or 3 awaits, with no particular culprit methods. The app is targeting .Net 4.6.1. This is a non-blocking issue as I was able to perform the role check prior to the awaits, put the result in a variable, and reference the variable after the awaits, but it was a very unexpected "gotcha" to work after 1-2 awaits, but not more. The code will be getting re-factored since the async calls aren't needed for those lookups, neither are returning the whole entities, but I'm still very curious if there is an explanation why the HttpContext would be "lost" after a couple of awaited tasks with .ConfigureAwait(false) was not used.
Update 1: The plot thickens.. I adjusted the test calls to add the test Boolean results to a List then repeated the set of loads for 5 iterations. I wanted to see if once it tripped to "False" if it ever returned to "True" at some point. My thinking is that the await was resuming on a different Thread that didn't have a reference to the current HttpContext, however no matter how many iterations I added, once false, it was always false. So next I tried repeating the first call (GetAllDecisions) 10 times... Surprisingly the first 10 iterations all came back as True?! So I took a more close look at varying the order to see if there were calls that were reliably tripping it up, and it turns out there were 3 of them.
so for instance:
var decisions = await _lookupService.GetAllDecisions();
results.Add(System.Web.HttpContext.Current != null);
decisions = await _lookupService.GetAllDecisions();
results.Add(System.Web.HttpContext.Current != null);
decisions = await _lookupService.GetAllDecisions();
results.Add(System.Web.HttpContext.Current != null);
returned True,True,True but then changing that to:
var eeoGroups = _lookupService.GetEEOGroups();
results.Add(System.Web.HttpContext.Current != null);
eeoGroups = _lookupService.GetEEOGroups();
results.Add(System.Web.HttpContext.Current != null);
eeoGroups = _lookupService.GetEEOGroups();
results.Add(System.Web.HttpContext.Current != null);
returned False, False, False.
Digging a little deeper I noticed that the methods were a mix of EntityFramework and older NHibernate-based repository code. It was the EntityFramework async methods that were tripping up the context on await.
One of the methods that trips up the Context after an await:
public async Task<List<string>> GetEEOGroups()
{
return await _dbContext.EmployeeEEOGroup.GroupBy(e => e.EEOGroup).Select(g => g.FirstOrDefault().EEOGroup).ToListAsync();
}
as did: *edit -whups that was a duplicate copy/paste:)
public async Task<IEnumerable<SapHHS>> GetAllHHS()
{
return await _dbContext.HHS.Where(x => x.IsActive).ToListAsync();
}
Yet this was fine:
public async Task<IEnumerable<Decision>> GetAllDecisions()
{
return await Task.FromResult(_repository.Session.QueryOver<Lookup>().Where(l => l.Type == "Decision" && l.IsActive).List().Select(l => new Decision { DecisionId = l.Id, Description = l.Name }).ToList());
}
Looking at the code that "works" it's pretty clear that it's not actually doing anything Async given the Task.FromResult against a synchronous method. I think the original authors were caught in the allure of the async silver-bullet and just wrapped the older code for consistency. EF's async methods work with await, but where async/await would seem to be supported with HttpContext.Current so long as <httpRuntime targetFramework="4.5" />, EF seems to trip this assumption up.