简体   繁体   中英

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, ie 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!

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!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM