代码之家  ›  专栏  ›  技术社区  ›  Steve Py

多次等待后,HTTPContext.Current在控制器中为空

  •  5
  • Steve Py  · 技术社区  · 7 年前

    .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() .

    测试1为真->测试7

    实际结果:

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

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

    更新一:情节越来越浓。。

    例如:

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

    再深入一点,我注意到这些方法是EntityFramework和旧的基于NHibernate的存储库代码的混合。是EntityFramework异步方法在await上绊倒了上下文。

    在等待之后触发上下文的方法之一:

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

    <httpRuntime targetFramework="4.5" /> ,EF似乎把这个假设弄错了。

    1 回复  |  直到 7 年前
        1
  •  2
  •   Barr J    7 年前

    blogs msdn 对这个问题进行了充分的研究,并对问题进行了分析,即找出了问题的根源:

    实例、请求和响应属性、会话(以及许多 其他)。如果没有这些信息,我们可以安全地说出我们的代码 不知道它正在执行的请求上下文。设计 完全无状态的web应用程序并不容易实现 这确实是一项具有挑战性的任务。此外,web应用程序具有丰富的 未指定的代码运行。

    HttpContext 在请求执行期间丢失。


    提供的解决方案:

    TaskScheduler ,的 ExecutionContext SynchronizationContext

    • 这个 任务调度器
      应用程序使用时,特定的任务调度器可能比
      优化吞吐量和并行后台处理。
    • 执行上下文 (EC)又一次以某种方式类似于这个名字所暗示的。您可以将其视为TLS(本地线程)的替代品
      它保证一个方法可以
      在不同的线程上中断和恢复而不造成伤害(都是从
      逻辑和安全观点)。要理解的关键是
      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,则继续异步运行。

      嵌套等待操作的任务内联行为
      (不影响LegacySpNetSynchronizationContext)


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