Reputation: 34773
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.
Upvotes: 5
Views: 2896
Reputation: 10929
In blogs msdn there is a full research regarding this issue and a mapping of the problem, I.E to the root of the trouble:
The HttpContext object stores all the request-relevant data, including the pointers to the native IIS request object, the ASP.NET pipeline instance, the Request and Response properties, the Session (and many others). Without all of this information, we can safely tell our code is unaware of the request context it is executing into. Designing entirely stateless web applications is not easy and implementing them is indeed a challenging task. Moreover, web applications are rich of third party libraries, which are generally black boxes where unspecified code runs.
It is a very interesting thing to ponder about, how such a crucial and fundamental object in the program, HttpContext
is lost during the request execution.
The soultion provided:
It consists of TaskScheduler
, the ExecutionContext
and the SynchronizationContext
:
- The
TaskScheduler
is exactly what you would expect. A scheduler for tasks! (I would be a great teacher!) Depending on the type of .NET
application used, a specific task scheduler might be better than
others. ASP.NET uses the ThreadPoolTaskScheduler by default, which is optimized for throughput and parallel background processing.- The
ExecutionContext
(EC) is again somehow similar to what the name suggests. You can look at it as a substitute of the TLS (thread local storage) for multithreaded parallel execution. In extreme synthesis,
it is the object used to persist all the environmental context needed for the code to run and it guarantees that a method can be
interrupted and resumed on different threads without harm (both from
a logical and security perspective). The key aspect to understand is
the EC needs to "flow" (essentially, be copied over from a thread to
another) whenever a code interrupt/resume occurs.- The
SynchronizationContext
(SC) is instead somewhat more difficult to grasp. It is related and in some ways similar to the EC, albeit enforcing a higher layer of abstraction. Indeed it can persist
environmental state, but it has dedicated implementations for
queueing/dequeuing work items in specific environments. Thanks to the SC, a developer can write code without bothering about how the
runtime handles the async/await patterns.
If you consider the code from the blog's example:
await DoStuff(doSleep, configAwait)
.ConfigureAwait(configAwait);
await Task.Factory.StartNew(
async () => await DoStuff(doSleep, configAwait)
.ConfigureAwait(configAwait),
System.Threading.CancellationToken.None,
asyncContinue ? TaskCreationOptions.RunContinuationsAsynchronously : TaskCreationOptions.None,
tsFromSyncContext ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Current)
.Unwrap().ConfigureAwait(configAwait);
The explenation:
configAwait
: controls the ConfigureAwait behavior when awaiting tasks (read on for additional considerations)tsFromSyncContext
: controls the TaskScheduler option passed to the StartNew method. If true, the TaskScheduler is built from the current SynchronizationContext, otherwise the Current TaskScheduler is used.doSleep
: if True, DoStuff awaits on a Thread.Sleep. If False, it awaits on a HttpClient.GetAsync operation Useful if you want to test
it without internet connectionasyncContinue
: controls the TaskCreationOptions passed to the StartNew method. If true, the continuations are run asynchronously.
Useful if you plan to test continuation tasks too and to assess the
behavior of task inlining in case of nested awaiting operations
(doesn't affect LegacyASPNETSynchronizationContext)
Dive into the article and see if it matches your issue, I believe you will find useful info inside.
There is another solution here, using nested container, you can check it as well.
Upvotes: 2