简体   繁体   English

DbContext 在 Async 方法中过早处理

[英]DbContext getting disposed too early within Async method

When converting an existing synchronous method to async, I accidentally used "async void" on one of the methods, which resulted in some unexpected behavior.在将现有同步方法转换为异步方法时,我不小心在其中一个方法上使用了“async void”,这导致了一些意外行为。

Below is a simplified example of the kind of change I had actually performed,以下是我实际执行的更改类型的简化示例,

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

The issue is that, the MakePostCallAsync() method, when trying to query the database using DbContext, throws an exception saying DbContext is already disposed.问题是, MakePostCallAsync()方法在尝试使用 DbContext 查询数据库时抛出异常,指出 DbContext 已被释放。

_dbContext in the example is injected using ASP .Net Core's DI (through AddDbContext() extension) with its default scope (Scoped). _dbContext中的_dbContext是使用 ASP .Net Core 的 DI(通过 AddDbContext() 扩展)及其默认范围 (Scoped) 注入的。

I fail to and need help understand the following,我未能并需要帮助理解以下内容,

  • why the DB context is disposed even before the request is served, while Scoped object's lifetime should be the entire duration of the current request为什么 DB 上下文甚至在请求被提供之前就被处理,而 Scoped 对象的生命周期应该是当前请求的整个持续时间
  • Even though I have used ConfigureAwait(true) (which explicitly means that once the awaited method returns MakePostCallAsync() should continue in Request context?) - this doesn't seem to be happening即使我使用了 ConfigureAwait(true)(这明确意味着一旦等待的方法返回 MakePostCallAsync() 应该在请求上下文中继续?) - 这似乎没有发生
  • In the actual code, this was fixed once I made all the methods async (all the way up to controller) - in the repro I have shared, even that doesn't help prevent the exception - why is it so, how do I solve this?在实际代码中,一旦我将所有方法都设为异步(一直到控制器),这个问题就得到了修复——在我共享的 repro 中,即使这样也无助于防止异常——为什么会这样,我该如何解决这个?

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

You should never use async void unless you are writing a event handler.除非您正在编写事件处理程序,否则永远不应使用async void If you want to use async/await you need to go all the way up the call stack till you get to return a Task<IActionResult>如果您想使用 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);
}

It seems to me you have some trouble understanding how to use async-await, because I see several major errors in your program.在我看来,您在理解如何使用 async-await 时遇到了一些麻烦,因为我在您的程序中看到了几个主要错误。

This article, written by the ever so helpful Stephen Cleary helped me to understand how to use it properly. 这篇文章由非常有帮助的 Stephen Cleary 撰写,帮助我了解如何正确使用它。

Every function that wants to use async-await has to return Task instead of void and Task<TResult> instead of TResult .每个想要使用 async-await 的函数都必须返回Task而不是voidTask<TResult>而不是TResult The only exception to this rule are event handlers that are not interested in the result of the actions.此规则的唯一例外是对操作结果不感兴趣的事件处理程序。

So first change Index such that it returns a Task<IActionResult> .因此,首先更改Index使其返回Task<IActionResult> Once you've done this, your compiler will probably warn you that you forgot to await somewhere inside your Index function.完成此操作后,您的编译器可能会警告您忘记在 Index 函数内的某处await

If you call an async function, you don't have to wait for it to finish, you can do other useful stuff, that will be performed whenever the async function has to await for something.如果您调用异步函数,则不必等待它完成,您可以做其他有用的事情,只要异步函数必须等待某事,就会执行这些操作。 But before you can use any result of the async function you have to await until it is ready:但是在你可以使用异步函数的任何结果之前,你必须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.

If your MakePostCallAsync would have returned something, for instance an int, the return value would have been Task<int> and your code would have been:如果您的 MakePostCallAsync 会返回某些内容,例如 int,则返回值将是Task<int>并且您的代码将是:

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

If you don't have something useful to do, just await immediately:如果您没有有用的事情要做,请立即等待:

int result = await MakePostCallAsync(...);

The reason for your exception is that your MakePostCallAsync is not finished completely before you somewhere Dispose your dbContext, probably via a using statement.您的异常的原因是您的 MakePostCallAsync 在您某处处置您的 dbContext 之前没有完全完成,可能是通过using语句。 After adding this await before returning you are certain that MakePostCallAsync is completely finished before returning Index()在返回之前添加此等待后,您可以确定 MakePostCallAsync 在返回 Index() 之前已完全完成

In your example you are not awaiting your call to MakePostCallAsync("http:\\\\google.com", vm) .在您的示例中,您不是在等待对MakePostCallAsync("http:\\\\google.com", vm)调用。 This means the request continues execution immediately and it eventually executes the code that disposes of your _dbContext , presumably while MakePostCallAsync is still waiting for the HTTP client to return a response.这意味着请求会立即继续执行,并最终执行处理_dbContext的代码,大概是在MakePostCallAsync仍在等待 HTTP 客户端返回响应的时候。 Once the HTTP client does return a response your MakePostCallAsync tries to call newmax = _dbContext.OnlyTable.Max(x => x.SomeColumn) but your request has already been handled and your DB context disposed of by then.一旦 HTTP 客户端确实返回响应,您的MakePostCallAsync尝试调用newmax = _dbContext.OnlyTable.Max(x => x.SomeColumn)但您的请求已经被处理并且您的数据库上下文到那时被处理。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM