【问题标题】:DbContext getting disposed too early within Async methodDbContext 在异步方法中过早释放
【发布时间】:2017-04-18 17:40:03
【问题描述】:

在将现有同步方法转换为异步时,我不小心在其中一个方法上使用了“async void”,这导致了一些意外行为。

以下是我实际执行的更改的简化示例,

public IActionResult Index()
{
    var vm = new ViewModel();
    try
    {
        var max = 0;

        if (_dbContext.OnlyTable.Any())
        {
            max = _dbContext.OnlyTable.Max(x => x.SomeColumn);
        }

        _dbContext.Add(new TestTable() { SomeColumn = max + 1 });
        _dbContext.SaveChanges();

        MakePostCallAsync("http:\\google.com", vm);

        if (!string.IsNullOrEmpty(vm.TextToDisplay))
        {
            vm.TextToDisplay = "I have inserted the value " + newmax + " into table (-1 means error)";
        }
        else
        {
            vm.TextToDisplay = "Errored!";
        }


    }
    catch (Exception ex)
    {
        vm.TextToDisplay = "I encountered error message - \"" + ex.Message + "\"";
    }
    return View("Index", vm);
}

private async void MakePostCallAsync(string url, ViewModel vm)
{
    var httpClient = new HttpClient();

    var httpResponse = await httpClient.PostAsync("http://google.com", null).ConfigureAwait(true);

    newmax = _dbContext.OnlyTable.Max(x => x.SomeColumn);
}

问题在于,MakePostCallAsync() 方法在尝试使用 DbContext 查询数据库时,会引发异常,指出 DbContext 已被释放。

示例中的_dbContext 是使用 ASP .Net Core 的 DI(通过 AddDbContext() 扩展)及其默认范围 (Scoped) 注入的。

我无法理解以下内容并需要帮助,

  • 为什么 DB 上下文在请求被处理之前就被释放了,而 Scoped 对象的生命周期应该是当前请求的整个持续时间
  • 即使我使用了 ConfigureAwait(true)(这明确意味着一旦等待的方法返回 MakePostCallAsync() 应该在请求上下文中继续?) - 这似乎没有发生
  • 在实际代码中,一旦我使所有方法异步(一直到控制器),这个问题就得到了修复 - 在我共享的 repro 中,即使这样也无助于防止异常 - 为什么会这样,如何我要解决这个问题吗?

Repro 在https://github.com/jjkcharles/SampleAsync 中可用

【问题讨论】:

    标签: c# entity-framework asynchronous asp.net-core async-await


    【解决方案1】:

    除非您正在编写事件处理程序,否则您不应该使用async void。如果你想使用 async/await,你需要一直向上调用堆栈,直到你返回一个 Task<IActionResult>

    public async Task<IActionResult> Index()
    {
        var vm = new ViewModel();
        try
        {
            var max = 0;
    
            if (_dbContext.OnlyTable.Any())
            {
                max = _dbContext.OnlyTable.Max(x => x.SomeColumn);
            }
    
            _dbContext.Add(new TestTable() { SomeColumn = max + 1 });
            _dbContext.SaveChanges();
    
            await MakePostCallAsync("http:\\google.com", vm);
    
            if (!string.IsNullOrEmpty(vm.TextToDisplay))
            {
                vm.TextToDisplay = "I have inserted the value " + newmax + " into table (-1 means error)";
            }
            else
            {
                vm.TextToDisplay = "Errored!";
            }
    
    
        }
        catch (Exception ex)
        {
            vm.TextToDisplay = "I encountered error message - \"" + ex.Message + "\"";
        }
        return View("Index", vm);
    }
    
    private async Task MakePostCallAsync(string url, ViewModel vm)
    {
        var httpClient = new HttpClient();
    
        var httpResponse = await httpClient.PostAsync("http://google.com", null).ConfigureAwait(true);
    
        newmax = _dbContext.OnlyTable.Max(x => x.SomeColumn);
    }
    

    【讨论】:

    • 谢谢,我确实意识到使用 async void 是不正确的。请参阅我的第三点,将所有内容更改为 async/await 并没有解决我的问题。 GitHub 存储库已将所有内容更改为异步,但仍然失败并出现相同的错误
    【解决方案2】:

    在我看来,您在理解如何使用 async-await 时有些困难,因为我发现您的程序中有几个重大错误。

    This article, 由乐于助人的 Stephen Cleary 撰写,帮助我了解如何正确使用它。

    每个想要使用 async-await 的函数都必须返回 Task 而不是 voidTask&lt;TResult&gt; 而不是 TResult。此规则的唯一例外是对操作结果不感兴趣的事件处理程序。

    所以首先更改Index,使其返回Task&lt;IActionResult&gt;。完成此操作后,您的编译器可能会警告您在 Index 函数中的某处忘记了 await

    如果您调用异步函数,您不必等待它完成,您可以做其他有用的事情,只要异步函数必须等待某事,就会执行这些事情。但在你可以使用异步函数的任何结果之前,你必须 await 直到它准备好:

    var taskMakePostCall = MakePostCallAsync(...)
    // if you have something useful to do, don't await
    DoSomethingUseful(...);
    // now you need the result of MakePostCallAsync, await until it is finished
    await taskMakePostCall;
    // now you  can use the results.
    

    如果您的 MakePostCallAsync 会返回一些内容,例如 int,则返回值将是 Task&lt;int&gt;,而您的代码将是:

    Task<int> taskMakePostCall = MakePostCallAsync(...)
    DoSomethingUseful(...);
    int result = await taskMakePostCall;
    

    如果你没有什么有用的事情要做,就立即等待:

    int result = await MakePostCallAsync(...);
    

    您的异常的原因是您的 MakePostCallAsync 在您在某处处置您的 dbContext 之前尚未完全完成,可能通过 using 语句。在返回之前添加此等待后,您可以确定 MakePostCallAsync 在返回 Index() 之前已完全完成

    【讨论】:

      【解决方案3】:

      在您的示例中,您不是在等待您致电MakePostCallAsync("http:\\google.com", vm)。这意味着请求会立即继续执行并最终执行处理您的_dbContext 的代码,大概是在MakePostCallAsync 仍在等待HTTP 客户端返回响应时。一旦 HTTP 客户端确实返回了响应,您的 MakePostCallAsync 会尝试调用 newmax = _dbContext.OnlyTable.Max(x =&gt; x.SomeColumn),但您的请求已经被处理并且您的数据库上下文到那时已被处理。

      【讨论】:

      • 但是,为什么我要求保留请求上下文(ConfigureAwait(True))后仍未保留它。此外,等待 MakePostCallAsync() 并没有改变代码的行为方式
      猜你喜欢
      • 2019-01-08
      • 1970-01-01
      • 2022-11-02
      • 2016-05-13
      • 2015-08-01
      • 1970-01-01
      • 2021-10-13
      • 2012-03-12
      • 1970-01-01
      相关资源
      最近更新 更多