8

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!

Community
  • 1
  • 1
aoven
  • 2,248
  • 2
  • 25
  • 36
  • 1
    Are you invoking `ConfigureAwait(false)` on your controller actions? – Paulo Morgado Apr 24 '17 at 10:56
  • @Paulo: No, I'm not. – aoven Apr 24 '17 at 11:14
  • 1
    @Eldho: What would be the point of that? I need and want async on controller action exactly because the internals (i.e. the DB call I mentioned, as one example) are async. Removing async would require me to disable an enormous amount of code. – aoven Apr 24 '17 at 11:32
  • Its just to check on non async . You can try `Task.FromResult(YourMethod)` for test. – Eldho Apr 24 '17 at 11:34
  • 3
    @Eldho How can a problem specific to `async` be tested without `async`? What will the testing show? That in a single-threaded non-async code `HttpContext.Current` does not lose the value? I'm already pretty sure that is the case. – GSerg Apr 24 '17 at 11:43
  • 1
    @Eldho: I'm not sure I follow, sorry. Are you talking about removing the async modifier on the controller method and getting rid of awaits in the body? If so, then I don't see how. I *need* to await the task, because I need to do something with its value before returning the final ActionResult. The task and the ActionResult are not directly connected. So I'm afraid I can't really do anything useful without the await that would warrant testing. – aoven Apr 24 '17 at 11:46
  • @aoven: You'll probably have to reduce it down to the smallest code base that can reproduce the problem. – Stephen Cleary Apr 24 '17 at 13:06

1 Answers1

2

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!

aoven
  • 2,248
  • 2
  • 25
  • 36
  • 3
    Another option would be to post the call to `SetResult` to the synchronization context. Something like: `_capturedContext.Post(_ => _source.SetResult(...), null);` – Kevin Gosse Apr 24 '17 at 20:43