DbContext 在 Async 方法中过早处理

jjk*_*les 2 c# asynchronous entity-framework async-await asp.net-core

在将现有同步方法转换为异步方法时,我不小心在其中一个方法上使用了“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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

我未能并需要帮助理解以下内容,

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

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

Sco*_*ain 6

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);
}
Run Code Online (Sandbox Code Playgroud)


Jas*_*oyd 0

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