ASP.NET 4.6异步控制器方法在等待后丢失HttpContext.Current

aov*_*ven 8 c# asp.net async-await

我有一个针对.NET 4.6的ASP.NET应用程序,我疯狂地试图找出为什么HttpContext.Current在我的异步MVC控制器操作中第一次等待之后变为null.

我检查并三重检查我的项目是针对v4.6,web.config的targetFramework属性也是4.6.

SynchronizationContext.Current在await之前和之后分配,它是正确的,即AspNetSynchronizationContext不是遗留的.

FWIW,await有问题确实在继续时切换线程,这可能是由于它调用外部I/O绑定代码(异步数据库调用)但这应该不是问题,AFAIU.

然而,它是!HttpContext.Current变为null 的事实导致我的代码出现了许多问题,对我来说没有任何意义.

我检查了通常的建议,我很肯定我正在做我应该做的一切.我的代码中也完全没有ConfigureAwait!

我所拥有的是,我的HttpApplication实例上有几个异步事件处理程序:

public MvcApplication()
{
    var helper = new EventHandlerTaskAsyncHelper(Application_PreRequestHandlerExecuteAsync);
    AddOnPreRequestHandlerExecuteAsync(helper.BeginEventHandler, helper.EndEventHandler);

    helper = new EventHandlerTaskAsyncHelper(Application_PostRequestHandlerExecuteAsync);
    AddOnPostRequestHandlerExecuteAsync(helper.BeginEventHandler, helper.EndEventHandler);
}
Run Code Online (Sandbox Code Playgroud)

我需要这两个因为自定义授权和清理逻辑,这需要异步.AFAIU,这是支持的,不应该是一个问题.

还有什么可能是我看到这种令人费解的行为的原因?

更新:补充观察.

SynchronizationContext在等待与等待之前,引用保持不变.但它的内部变化在下面的截图中可以看到!

在AWAIT之前: 在进入等待之前

等待之后: 继续之后

我不确定这与我的问题有什么关系(或者甚至是否).希望别人能看到它!

aov*_*ven 2

我决定定义一个手表HttpContext.Current,并开始“进入”等待状态,看看它到底在哪里发生了变化。毫不奇怪,随着我的继续,线程被切换了多次,这对我来说很有意义,因为途中有多个真正的异步调用。他们都HttpContext.Current按照应有的方式保存了实例。

然后我就犯规了……

var observer = new EventObserver();
using (EventMonitor.Instance.Observe(observer, ...))
{
    await plan.ExecuteAsync(...);
}

var events = await observer.Task; // Doh!
Run Code Online (Sandbox Code Playgroud)

简短的解释是plan.ExecuteAsync执行许多步骤,这些步骤通过专用线程以非阻塞方式报告给专用事件日志。这是商业软件,报告事件的模式在整个代码中被广泛使用。大多数时候,这些事件与调用者没有直接关系。但有一两个地方比较特殊,因为调用者想知道由于执行特定代码而发生了哪些事件。这就是EventObserver使用实例的时候,如上所示。

await observer.Task为了等待所有相关事件被处理和观察,这是必要的。所讨论的任务来自TaskCompletionSource观察者拥有的实例。一旦所有事件逐渐传入,SetResult就会从处理事件的线程调用源。我最初对此细节的实现非常天真,如下所示:

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(...);
    }
}
Run Code Online (Sandbox Code Playgroud)

SetSynchronizationContext我现在可以看到,之前的调用SetResult并没有达到我希望的效果。目标是将原始同步上下文应用于该行的延续await observer.Task

现在的问题是:我该如何正确地做到这一点?我猜它会ContinueWith在某个地方进行显式调用。

更新

这就是我所做的。我传递了TaskCreationOptions.RunContinuationsAsynchronouslyTaskCompletionSource 构造函数选项,并修改了类上的 Task 属性EventObserver以包含显式同步的延续:

public Task<T> Task
{
    get
    {
        return _source.Task.ContinueWith(t =>
        {
            if (_capturedContext != null)
            {
                SynchronizationContext.SetSynchronizationContext(_capturedContext);
            }

            return t.Result;
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

所以现在,当代码调用 时await observer.Task,延续将确保首先输入正确的上下文。到目前为止,它似乎工作正常!

  • 另一种选择是将对“SetResult”的调用发布到同步上下文。类似于:`_capturedContext.Post(_ =&gt; _source.SetResult(...), null);` (3认同)