Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET 4.6 async controller method loses HttpContext.Current after await

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: Before entering await

AFTER AWAIT: After continuing

I'm not sure how (or even if) this might be relevant to my problem at this point. Hopefully someone else can see it!

like image 247
aoven Avatar asked Apr 24 '17 10:04

aoven


People also ask

Why is HttpContext current null after await?

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.

Why HttpContext current is null?

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.

What can I use instead of HttpContext current?

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.

What is HttpContext current session?

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.


Video Answer


1 Answers

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!

like image 179
aoven Avatar answered Nov 15 '22 00:11

aoven