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

时间:2018-10-24 04:40:16

标签: 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
    // ...

调用本身是通过同一查找服务针对EF的简单查询。假设它们是EF并且使用相同的UoW / DbContext,则不能将它们更改为使用Task.WhenAll()

预期结果: 适用于test1-> test7

实际结果: 对于test1为True,-> test3,对于test4为False-> test7

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

我改变了方法的顺序,但在等待2或3次后都失败了,没有特别的罪魁祸首。该应用的目标是.Net 4.6.1。这是一个非阻塞性问题,因为我能够在等待之前执行角色检查,将结果放入变量中,并在等待之后引用该变量,但这是一个非常意外的“陷阱”,无法在1-之后工作2等待,但不更多。由于这些查找不需要异步调用,也不会返回整个实体,因此代码将得到重构,但是我仍然很好奇是否有解释为什么在几遍之后HttpContext会“丢失”的原因未使用带有.ConfigureAwait(false)的等待任务。

更新1:地块变厚。 我调整了测试调用,以将测试布尔结果添加到列表中,然后重复进行5次迭代的加载。我想看看它是否曾经在某个时候返回“ True”,是否跳到了“ False”。我的想法是,等待是在没有引用当前HttpContext的另一个线程上继续进行的,但是无论我添加了多少次迭代(一旦为false),它总是为false。因此,接下来我尝试将第一次调用(GetAllDecisions)重复10次...令人惊讶的是,前10次迭代全部返回为True ?!因此,我更仔细地研究了更改顺序,以查看是否有可靠地将其触发的呼叫,结果发现其中有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);

返回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);

返回False,False,False。

深入研究,我注意到这些方法是EntityFramework和较旧的基于NHibernate的存储库代码的混合。正是EntityFramework异步方法在等待时触发了上下文。

等待后触发Context的方法之一:

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

一样:*编辑-whups,它是重复的副本/粘贴:)

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

这还好:

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());
}

查看“有效”的代码,很明显,在给定Task.FromResult针对同步方法的情况下,它实际上并未执行任何异步操作。我认为原始作者被异步子弹的魅力所吸引,只是为了保持一致性而包装了较旧的代码。 EF的async方法可与await一起使用,但是HttpContext似乎支持async / await。当前只要<httpRuntime targetFramework="4.5" />,EF似乎都会使这一假设无效。

1 个答案:

答案 0 :(得分:2)

blogs msdn中,有一个关于此问题的完整研究以及该问题的映射,即问题根源:

  

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

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


提供的灵魂

它由TaskSchedulerExecutionContextSynchronizationContext组成:

  
      
  • TaskScheduler正是您所期望的。任务调度器! (我会是一个很棒的老师!)取决于.NET的类型
      应用程序,特定的任务计划程序可能比
      其他。默认情况下,ASP.NET使用ThreadPoolTask​​Scheduler,即   针对吞吐量和并行后台处理进行了优化。
  •   
  • ExecutionContext(EC)再次与名称所暗示的相似。您可以将其视为TLS(本地线程   存储),用于多线程并行执行。在极端综合中,
      它是用于保留所有所需环境上下文的对象   来确保代码可以运行
      在不同线程上中断和恢复而不会造成伤害(均来自
      逻辑和安全角度)。要理解的关键方面是
      EC需要“流动”(基本上是从线程复制到
      另一种),只要发生代码中断/恢复。
  •   
  • SynchronizationContext(SC)有点难以掌握。它与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);  

曝光:

  
      
  • configAwait:控制等待任务时的ConfigureAwait行为(请继续阅读以获取其他注意事项)
  •   
  • tsFromSyncContext:控制传递给StartNew方法的TaskScheduler选项。如果为true,则TaskScheduler是从当前版本构建的   SynchronizationContext,否则使用当前TaskScheduler。
  •   
  • doSleep:如果为True,则DoStuff等待Thread.Sleep。如果为False,它将在HttpClient.GetAsync操作上等待。如果要测试,则有用
      它没有互联网连接
  •   
  • asyncContinue:控制传递给StartNew方法的TaskCreationOptions。如果为true,则继续以异步方式运行。
      如果您还计划测试继续任务并评估
      嵌套等待操作中任务内联的行为
      (不会影响LegacyASPNETSynchronizationContext)
  •   

深入研究本文,看看它是否与您的问题匹配,我相信您会在其中找到有用的信息。


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