DbContext在Async方法中过早处理

时间:2016-12-04 04:39:15

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

将现有的同步方法转换为异步时,我意外地使用了" 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()应该在Request上下文中继续?) - 这似乎没有发生
  • 在实际代码中,一旦我将所有方法设置为异步(一直到控制器),这一问题就解决了 - 在我共享的repro中,即使这样做也没有帮助防止异常 - 为什么会这样,我该如何解决这个问题?

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

中找到

2 个答案:

答案 0 :(得分:0)

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

答案 1 :(得分:0)

在我看来,您在理解如何使用async-await时遇到一些麻烦,因为我在您的程序中看到了几个主要错误。

由非常有帮助的Stephen Cleary写的{p> This article,帮助我理解了如何正确使用它。

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

首先更改Index,使其返回Task<IActionResult>。完成此操作后,编译器可能会警告您忘记等待Index函数内的某个位置。

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

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<int>而你的代码就是:

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

如果你没有什么有用的东西,请立即等待:

int result = await MakePostCallAsync(...);

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