在通过 DI 创建的对象的构造函数中使用的 DbContext 的异步问题

Async Issue for DbContext used in constructor of objects created via DI

我想知道是否有人可以阐明什么时候等待什么时候不等待。考虑这段代码

public Task<List<User>> GetUsersForParent(int someParentId)
{
    var qry = Context.Users.Where(u=>u.parent = someParentId)
                   .OrderBy(u=>u.Surname)
    return FilterActive(qry);
}


//Actually in a generic base class, but not important (I don't think)
protected Task<List<T>> FilterActive(IQueryable<T> query) where T: BaseEntity
{
    return query.Where( q=>q.Active == true ).ToListAsync();
}

然后就是这样使用

var users = await DbHandler.GetUsersForParent(1);

因此等待调用方法,但其他方法不是。这是正确的吗?

是否应该等待调用 ToListAsync() 的方法? (我假设现在正在做这项工作)

我这样做的原因是我发现 DbContext 正被第二个线程使用,出现可怕的异常。我运行没地方看了。我的理解是这些方法正在构建要执行的整个任务,但这会不会扰乱 dbContext?

重新编辑 DbContext 错误

通过 Debug.Print 和 SQL 查询分析缩小了问题的潜在位置(以防万一对其他人有帮助)我可以看到一个正在分析的语句(配置文件中的下一个正在记录异常),我可以通过调试打印看到两种方法 运行。

其中一个方法是 PermissionsManager,它在构造时初始化自身并加载用户数据。这是在通过 DI 框架请求时构建的。

另一种方法是对页面的 OnGet() 方法进行单一查询。 运行通过 ID 获取实体的单个查询,正确等待。

我目前的工作理论是线程 运行 进行 DI 构造和另一个线程 运行 进行页面初始化正在发生冲突。

当我只_person = new Person() // await db.users.get(userid) 设置 PermissionManager 时,问题就消失了。我可以在 2 或 3 次刷新中复制问题 1,尽管刷新页面 30 次以上,但我无法复制该评论。

所以我对 async / await 的真正问题可能更多是关于 DI 注入,并且 运行 在不同的线程上构建?如果是这样,有什么最佳做法可以避免吗?

Should the method calling the ToListAsync() be awaited? (this I assume is now doing the work)

如果您 await 任一方法的内容,您将 return 结果类型,而不是 Task 结果类型,这意味着无法延迟执行。

你的错误将会出现,因为你有多个线程与同一个 DbContext 实例交互,等待或没有这会导致问题,或者你有一些代码调用包含 ToListAsync() 的方法,或另一个不等待的异步 DbContext 操作。

编写 EF 数据访问层 returning Task 相当危险,很容易搬起石头砸自己的脚。

鉴于您的代码结构,我建议进行一些小改动:

public async Task<List<User>> GetUsersForParent(int someParentId)
{
    var qry = Context.Users.Where(u=>u.parent = someParentId)
                   .OrderBy(u=>u.Surname);
    qry = FilterActive(qry);
    return await qry.ToListAsync();
}

protected IQueryable<T> FilterActive(IQueryable<T> query) where T: BaseEntity
{
    return query.Where( q=> q.Active == true );
}

特别是在这里我会避免 returning Task 以减少不当使用和潜在的间歇性错误的风险。 FilterActive 的base-class 方法可以return IQueryable<T> 应用过滤器而不触发操作的执行。这种方式 FilterActive 可以应用,无论你想要一个列表,一个计数,还是简单地做一个存在检查。

总的来说,我建议探索 return IQueryable<TEntity> 而不是 List<TEntity> 等的模式,因为后者要么对性能和灵活性有很多限制,要么需要很多用于处理诸如以下内容的样板代码:

  • 正在排序,
  • 分页,
  • 只得到一个计数,
  • 正在执行 Exists 检查,
  • 可配置过滤,
  • 有选择地预先加载相关数据,或
  • 生成高效查询的投影

使用 return List<TEntity> 的方法执行此操作会导致非常复杂的代码以支持上述某些考虑因素,是否应用了这些操作 post - 执行导致比查询更重否则将需要,或者需要大量近乎重复的代码来处理每个场景。

So the calling method is awaited, but the others are not. Is this correct?

我通常建议使用 asyncawait 关键字,以及 only return the tasks directly if the method is extremely simple.

My reason for this is I am getting the DbContext is being used by a second thread dreaded exception. I am running out of places to look. My understanding is the methods are building up the whole task which is executed, but could this be messing with the dbContext?

没有。至少,您发布的代码不会导致该异常。无论是使用 async/await 关键字,还是直接返回任务,这些方法都是异步的,它们不会尝试一次在 dbcontext 上做不止一件事情。

您的问题可能更上层。 Task.WhenAll 是个很好的搜索工具。

所以构造函数是一个转移注意力的东西。毕竟这是一个缺失的 await,只是不在预期的位置并且代码没有改变。

我找到了罪魁祸首。 basePage 中有一个方法挂接到 MVC 页面的过滤器中。它需要用户并加载他们的权限,但是,由于这种用户权限加载是异步的,因此不会等待此方法(之前不需要它,因为它是同步的)。我将其移至页面生命周期中的异步事件之一,现在一切似乎都很愉快(有合适的​​等待!)。所以这是一个缺失的等待,但这个故事的寓意是任何时候你将同步方法设为异步,检查到底是什么在使用它!