【问题标题】:ASP.NET 4.6 async controller method loses HttpContext.Current after awaitASP.NET 4.6 异步控制器方法在等待后丢失 HttpContext.Current
【发布时间】:2017-09-20 23:51:23
【问题描述】:

我有一个面向 .NET 4.6 的 ASP.NET 应用程序,我正在疯狂地试图弄清楚为什么 HttpContext.Current 在我的异步 MVC 控制器操作中的第一次等待之后变为空。

我已经检查并三重检查了我的项目的目标是 v4.6,并且 web.config 的 targetFramework 属性也是 4.6。

SynchronizationContext.Current 在 await 之前和之后都分配,它是正确的,即 AspNetSynchronizationContext,而不是旧的。

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

然而,确实如此! HttpContext.Current 变为 null 的事实给我的代码带来了许多问题,这对我来说没有任何意义。

我检查了the usual recommendations,我很肯定我正在做我应该做的一切。我的代码中也绝对没有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);
}

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

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

更新:补充观察。

SynchronizationContext 引用在 await 之后与 await 之前保持不变。但它的内部结构在两者之间发生了变化,如下面的截图所示!

等待之前:

等待之后:

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

【问题讨论】:

  • 您是否在控制器操作上调用ConfigureAwait(false)
  • @Paulo:不,我不是。
  • @Eldho:那有什么意义呢?我需要并且想要控制器操作上的异步,正是因为内部(即我提到的数据库调用,作为一个例子)是异步的。删除异步需要我禁用大量代码。
  • @Eldho 如何在没有async 的情况下测试async 特有的问题?测试会显示什么?那在单线程非异步代码中HttpContext.Current 不会失去价值?我已经很确定是这样的。
  • @Eldho:我不确定我是否跟随,抱歉。您是在谈论删除控制器方法上的 async 修饰符并摆脱主体中的等待吗?如果是这样,那么我不知道如何。我需要等待任务,因为我需要在返回最终的 ActionResult 之前对其值做一些事情。任务和 ActionResult 没有直接联系。因此,如果没有需要测试的等待,恐怕我真的无法做任何有用的事情。

标签: c# asp.net async-await


【解决方案1】:

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

然后我触犯了问题......

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

var events = await observer.Task; // Doh!

简短的解释是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(...);
    }
}

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

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

更新

这就是我所做的。我将 TaskCreationOptions.RunContinuationsAsynchronously 选项传递给 TaskCompletionSource ctor,并修改了我的 EventObserver 类的 Task 属性以包含显式同步延续:

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

            return t.Result;
        });
    }
}

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

【讨论】:

  • 另一种选择是将调用SetResult 发布到同步上下文。比如:_capturedContext.Post(_ =&gt; _source.SetResult(...), null);
猜你喜欢
  • 2018-07-27
  • 2015-12-20
  • 2015-07-05
  • 2012-10-14
  • 2014-05-15
  • 2015-04-26
  • 2015-09-01
  • 1970-01-01
  • 2018-03-12
相关资源
最近更新 更多