多次等待后,控制器中的HTTPContext.Current为null

Ste*_* Py 5 c# asp.net-mvc async-await

这是我在一些遗留代码中遇到的一个奇怪的问题,我已将其标记为重新考虑因素.我检查过它,代码没有使用.ConfigureAwait(false)..

有问题的代码看起来很像:("testx = ...."行是调试问题以暴露行为的一部分.)

public async Task<ActionResult> Index()
{
    ValidateRoleAccess(Roles.Admin, Roles.AuthorizedUser, Roles.AuditReadOnly);

    var test1 = System.Web.HttpContext.Current != null
    var decisions = await _lookupService.GetAllDecisions();
    var test2 = System.Web.HttpContext.Current != null
    var statuses = await _lookupService.GetAllEnquiryStatuses();
    var test3 = System.Web.HttpContext.Current != null
    var eeoGroups = await _lookupService.GetEEOGroups();
    var test4 = System.Web.HttpContext.Current != null
    var subCategories = await _lookupService.GetEnquiryTypeSubCategories();
    var test5 = System.Web.HttpContext.Current != null
    var paystreams = await _lookupService.GetPaystreams();
    var test6 = System.Web.HttpContext.Current != null
    var hhses = await _lookupService.GetAllHHS();
    var test7 = System.Web.HttpContext.Current != null
    // ...
Run Code Online (Sandbox Code Playgroud)

调用本身是通过相同的查找服务对EF的简单查询.鉴于它们是EF并使用相同的UoW/DbContext,这些不能更改使用Task.WhenAll().

预期结果:对于test1 - > test7为真

实际结果:对于test1为真, - > test3,对于test4 - > test7为假

在等待查找调用之后,我在针对特定角色添加了验证时发现了该问题.该检查在HttpContext.Current上触发了一个空引用异常,验证方法使用该异常.因此它在异步之前在ValidateRoleAccess调用中使用时传递,但在所有等待的方法之后调用时失败.

我改变了方法的顺序,在2或3等待之后它失败了,没有特别的罪魁祸首方法.该应用程序的目标是.Net 4.6.1.这是一个非阻塞问题,因为我能够在等待之前执行角色检查,将结果放在变量中,并在等待之后引用变量,但是在1-之后它是一个非常意外的"陷阱". 2等待,但不是更多.代码将被重新计算,因为这些查找不需要异步调用,也没有返回整个实体,但是我仍然很好奇,如果有一个解释为什么HttpContext会在一对夫妇之后"丢失"没有使用.ConfigureAwait(false)的等待任务.

更新1:图表变浓..我调整了测试调用,将测试布尔结果添加到List,然后重复5次迭代的加载集.如果它曾经在某个时刻恢复到"真实"状态,我想知道它是否一旦跳到"假".我的想法是await在一个没有引用当前HttpContext的不同Thread上恢复,但无论我添加多少次迭代,一旦错误,它总是假的.接下来我尝试重复第一次调用(GetAllDecisions)10次......令人惊讶的是前10次迭代都回归真实?!所以我更仔细地看一下改变顺序,看看是否有可靠的绊倒它的电话,结果发现其中有3个.

例如:

var decisions = await _lookupService.GetAllDecisions();
results.Add(System.Web.HttpContext.Current != null);
decisions = await _lookupService.GetAllDecisions();
results.Add(System.Web.HttpContext.Current != null);
decisions = await _lookupService.GetAllDecisions();
results.Add(System.Web.HttpContext.Current != null);
Run Code Online (Sandbox Code Playgroud)

返回True,True,True然后将其更改为:

var eeoGroups = _lookupService.GetEEOGroups();
results.Add(System.Web.HttpContext.Current != null);
eeoGroups = _lookupService.GetEEOGroups();
results.Add(System.Web.HttpContext.Current != null);
eeoGroups = _lookupService.GetEEOGroups();
results.Add(System.Web.HttpContext.Current != null);
Run Code Online (Sandbox Code Playgroud)

返回False,False,False.

深入挖掘我注意到这些方法是EntityFramework和旧的基于NHibernate的存储库代码的混合.EntitFramework异步方法正在等待上下文.

在等待之后使Context上升的方法之一:

public async Task<List<string>> GetEEOGroups()
{
    return await _dbContext.EmployeeEEOGroup.GroupBy(e => e.EEOGroup).Select(g => g.FirstOrDefault().EEOGroup).ToListAsync();
}
Run Code Online (Sandbox Code Playgroud)

同样如下:*编辑-whups是重复复制/粘贴:)

public async Task<IEnumerable<SapHHS>> GetAllHHS()
{
    return await _dbContext.HHS.Where(x => x.IsActive).ToListAsync();
}
Run Code Online (Sandbox Code Playgroud)

然而这很好:

public async Task<IEnumerable<Decision>> GetAllDecisions()
{
    return await Task.FromResult(_repository.Session.QueryOver<Lookup>().Where(l => l.Type == "Decision" && l.IsActive).List().Select(l => new Decision { DecisionId = l.Id, Description = l.Name }).ToList());
}
Run Code Online (Sandbox Code Playgroud)

看看"工作"的代码很明显,它实际上并没有针对同步方法给出Task.FromResult的Async.我认为原始作者陷入了异步银弹的诱惑之中,只是将旧代码包装起来以保持一致性.EF的异步方法可以使用await,但是在HttpContext.Current似乎支持async/await的情况下,只要<httpRuntime targetFramework="4.5" />EF看起来可以推测这个假设.

Bar*_*r J 2

msdn 博客中,有关于此问题的完整研究以及问题、IE 到问题根源的映射:

HttpContext 对象存储所有与请求相关的数据,包括指向本机 IIS 请求对象的指针、ASP.NET 管道实例、Request 和 Response 属性、Session(以及许多其他数据)。如果没有所有这些信息,我们可以放心地告诉我们的代码不知道它正在执行的请求上下文。设计完全无状态的 Web 应用程序并不容易,实现它们确实是一项具有挑战性的任务。此外,Web 应用程序包含丰富的第三方库,这些第三方库通常是运行未指定代码的黑匣子。

思考一个非常有趣的事情,程序中如此重要和基本的对象HttpContext是如何在请求执行过程中丢失的。


灵魂提供:

它由TaskSchedulerExecutionContext和组成SynchronizationContext

  • TaskScheduler正是您所期望的。任务调度程序!(我会成为一名出色的老师!)根据所使用的 .NET 应用程序的类型
    ,特定的任务调度程序可能比其他任务调度程序更好
    。ASP.NET 默认使用 ThreadPoolTask​​Scheduler,它针对吞吐量和并行后台处理进行了优化。
  • ( ExecutionContextEC) 在某种程度上又与它的名字所暗示的相似。您可以将其视为多线程并行执行的 TLS(线程本地存储)的替代品。在极端综合中,
    它是用于保存代码运行所需的所有环境上下文的对象,并且它保证可以
    在不同线程上中断和恢复方法而不会造成损害(从
    逻辑和安全角度来看)。要理解的关键方面是,每当发生代码中断/恢复时
    ,EC 都需要“流动”(本质上是从一个线程复制到
    另一个线程)。
  • 相反, ( SynchronizationContextSC) 更难掌握。它与 EC 相关且在某些方面相似,尽管强制执行更高的抽象层。事实上,它可以持久保存
    环境状态,但它有专门的实现来
    在特定环境中对工作项进行排队/出队。借助 SC,开发人员可以编写代码,而不必担心
    运行时如何处理异步/等待模式。

如果您考虑博客示例中的代码:

   await DoStuff(doSleep, configAwait)
        .ConfigureAwait(configAwait);

    await Task.Factory.StartNew(
        async () => await DoStuff(doSleep, configAwait)
            .ConfigureAwait(configAwait),
        System.Threading.CancellationToken.None,
        asyncContinue ? TaskCreationOptions.RunContinuationsAsynchronously : TaskCreationOptions.None,
        tsFromSyncContext ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Current)
        .Unwrap().ConfigureAwait(configAwait);  
Run Code Online (Sandbox Code Playgroud)

解释:

  • configAwait:控制等待任务时的ConfigureAwait行为(请继续阅读以了解更多注意事项)
  • tsFromSyncContext:控制传递给 StartNew 方法的 TaskScheduler 选项。如果为 true,则 TaskScheduler 是从当前 SynchronizationContext 构建的,否则使用 Current TaskScheduler。
  • doSleep:如果为 True,则 DoStuff 等待 Thread.Sleep。如果为 False,则它等待 HttpClient.GetAsync 操作 如果您想
    在没有互联网连接的情况下测试它,则很有用
  • asyncContinue:控制传递给 StartNew 方法的 TaskCreationOptions。如果为 true,则延续将异步运行。
    如果您计划也测试延续任务并评估
    嵌套等待操作时任务内联的行为
    (不影响 LegacyASPNETSynchronizationContext),则非常有用

深入研究这篇文章,看看它是否符合您的问题,我相信您会在里面找到有用的信息。


这里还有另一种解决方案,使用嵌套容器,您也可以检查它。