I have an ASP.NET app targeting .NET 4.6 and I'm going crazy trying to figure out why HttpContext.Current
becomes null after the first await inside my async MVC controller action.
I've checked and triple-checked that my project is targeting v4.6 and that the web.config's targetFramework
attribute is 4.6 as well.
SynchronizationContext.Current
is assigned both before and after the await and it's the right one, i.e. AspNetSynchronizationContext
, not the legacy one.
FWIW, the await in question does switch threads on continuation, which is probably due to the fact that it invokes external I/O-bound code (an async database call) but that shouldn't be a problem, AFAIU.
And yet, it is! The fact that HttpContext.Current
becomes null causes a number of problems for my code down the line and it doesn't make any sense to me.
I've checked the usual recommendations and I'm positive I'm doing everything I should be. I also have absolutely no ConfigureAwait
's in my code!
What I DO have, is a couple of async event handlers on my HttpApplication
instance:
public MvcApplication()
{
var helper = new EventHandlerTaskAsyncHelper(Application_PreRequestHandlerExecuteAsync);
AddOnPreRequestHandlerExecuteAsync(helper.BeginEventHandler, helper.EndEventHandler);
helper = new EventHandlerTaskAsyncHelper(Application_PostRequestHandlerExecuteAsync);
AddOnPostRequestHandlerExecuteAsync(helper.BeginEventHandler, helper.EndEventHandler);
}
I need these two because of custom authorization & cleanup logic, which requires async. AFAIU, this is supported and shouldn't be a problem.
What else could possibly be the reason for this puzzling behavior that I'm seeing?
UPDATE: Additional observation.
The SynchronizationContext
reference stays the same after await vs. before await. But its internals change in between as can be seen in screenshots below!
BEFORE AWAIT:
AFTER AWAIT:
I'm not sure how (or even if) this might be relevant to my problem at this point. Hopefully someone else can see it!
Your test is not flawed and HttpContext. Current should not be null after the await because in ASP.NET Web API when you await, this will ensure that the code that follows this await is passed the correct HttpContext that was present before the await.
It won't work in the scheduling related class because relevant code is not executed on a valid thread, but a background thread, which has no HTTP context associated with. Overall, don't use Application["Setting"] to store global stuffs, as they are not global as you discovered.
Instead of using HttpContext. Current , use the HttpContext provided as a property on the Page or Controller , or even better, you can simply use the Session property.
HttpContext. Current. Session simply returns null if there is no session available. The HttpApplication's implementation of the Session property throws an HttpException with the message Session state is not available in this context.
I decided to define a watch on HttpContext.Current
and started stepping "into" the await to see where exactly it changes. To no surprise, the thread was switched multiple times as I went on, which made sense to me because there were multiple true async calls on the way. They all preserved the HttpContext.Current
instance as they are supposed to.
And then I hit the offending line...
var observer = new EventObserver();
using (EventMonitor.Instance.Observe(observer, ...))
{
await plan.ExecuteAsync(...);
}
var events = await observer.Task; // Doh!
The short explanation is that plan.ExecuteAsync
performs a number of steps which are reported to a specialized event log in a non-blocking manner via a dedicated thread. This being business software, the pattern of reporting events is quite extensively used throughout the code. Most of the time, these events are of no direct concern to the caller. But one or two places are special in that the caller would like to know which events have occurred as a result of executing a certain code. That's when an EventObserver
instance is used, as seen above.
The await observer.Task
is necessary in order to wait for all relevant events to be processed and observed. The Task in question comes from a TaskCompletionSource
instance, owned by the observer. Once all events have trickled in, the source's SetResult
is called from a thread that processed the events. My original implementation of this detail was - very naively - as follows:
public class EventObserver : IObserver<T>
{
private readonly ObservedEvents _events = new ObservedEvents();
private readonly TaskCompletionSource<T> _source;
private readonly SynchronizationContext _capturedContext;
public EventObserver()
{
_source = new TaskCompletionSource<T>();
// Capture the current synchronization context.
_capturedContext = SynchronizationContext.Current;
}
void OnCompleted()
{
// Apply the captured synchronization context.
SynchronizationContext.SetSynchronizationContext(_capturedContext);
_source.SetResult(...);
}
}
I can now see that calling SetSynchronizationContext
before SetResult
isn't doing what I hoped it would be. The goal was to apply the original synchronization context to the continuation of the line await observer.Task
.
The question now is: how do I do that properly? I'm guessing it will take an explicit ContinueWith
call somewhere.
UPDATE
Here's what I did. I passed the TaskCreationOptions.RunContinuationsAsynchronously
option the TaskCompletionSource ctor and modified the Task property on my EventObserver
class to include explicitly synchronized continuation:
public Task<T> Task
{
get
{
return _source.Task.ContinueWith(t =>
{
if (_capturedContext != null)
{
SynchronizationContext.SetSynchronizationContext(_capturedContext);
}
return t.Result;
});
}
}
So now, when a code calls await observer.Task
, the continuation will make sure the correct context is entered first. So far, it seems to be working correctly!
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