本文介绍了使用 Entity Framework Core 的 Blazor 并发问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的目标

我想创建一个新的 IdentityUser 并显示已通过同一个 Blazor 页面创建的所有用户.该页面有:

I want to create a new IdentityUser and show all the users already created through the same Blazor page. This page has:

  1. 一个通过你的表单将创建一个 IdentityUser
  2. 第三方的网格组件 (DevExpress Blazor DxDataGrid),显示所有使用 UserManager.Users 属性的用户.此组件接受 IQueryable 作为数据源.

问题

当我通过表单(1)创建一个新用户时,会出现以下并发错误:

When I create a new user through the form (1) I will get the following concurrency error:

InvalidOperationException:在上一个操作完成之前,在此上下文中启动了第二个操作.不保证任何实例成员都是线程安全的.

我认为问题与 CreateAsync(IdentityUser user)UserManager.Users 指的是相同的 DbContext

I think the problem is related to the fact that CreateAsync(IdentityUser user) and UserManager.Users are referring the same DbContext

这个问题与第三方的组件无关,因为我重现了同样的问题,用一个简单的列表替换它.

The problem isn't related to the third-party's component because I reproduce the same problem replacing it with a simple list.

重现问题的步骤

  1. 使用身份验证创建一个新的 Blazor 服务器端项目
  2. 使用以下代码更改 Index.razor:

  1. create a new Blazor server-side project with authentication
  2. change Index.razor with the following code:

@page "/"

<h1>Hello, world!</h1>

number of users: @Users.Count()
<button @onclick="@(async () => await Add())">click me</button>
<ul>
@foreach(var user in Users)
{
    <li>@user.UserName</li>
}
</ul>

@code {
    [Inject] UserManager<IdentityUser> UserManager { get; set; }

    IQueryable<IdentityUser> Users;

    protected override void OnInitialized()
    {
        Users = UserManager.Users;
    }

    public async Task Add()
    {
        await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
    }
}

我注意到的

  • 如果我将实体框架提供程序从 SqlServer 更改为 Sqlite,则永远不会显示错误.

系统信息

  • ASP.NET Core 3.1.0 Blazor 服务器端
  • Entity Framework Core 3.1.0 基于 SqlServer 提供程序

我已经看到的

  • Blazor A second operation started on this context before a previous operation completed: the solution proposed doesn't work for me because even if I change my DbContext scope from Scoped to Transient I still using the same instance of UserManager and its contains the same instance of DbContext
  • other guys on StackOverflow suggests creating a new instance of DbContext per request. I don't like this solution because it is against Dependency Injection principles. Anyway, I can't apply this solution because DbContext is wrapped inside UserManager
  • Create a generator of DbContext: this solution is pretty like the previous one.
  • Using Entity Framework Core with Blazor

为什么我要使用 IQueryable

我想传递一个 IQueryable 作为我第三方组件的数据源,因为它可以直接对查询应用分页和过滤.此外 IQueryable 对 CUD 敏感操作.

I want to pass an IQueryable as a data source for my third-party's component because its can apply pagination and filtering directly to the Query. Furthermore IQueryable is sensitive to CUD operations.

推荐答案

UPDATE (08/19/2020)

UPDATE (08/19/2020)

在这里您可以找到 有关如何同时使用 Blazor 和 EFCore 的文档

更新 (07/22/2020)

UPDATE (07/22/2020)

EFCore 团队在 Entity Framework Core .NET 5 Preview 7 中引入了 DbContextFactory

EFCore team introduces DbContextFactory inside Entity Framework Core .NET 5 Preview 7

[...] 这种解耦对于 Blazor 应用程序非常有用,推荐使用 IDbContextFactory,但在其他场景中也可能有用.

如果您有兴趣,可以在 宣布 Entity Framework Core EF Core 5.0 Preview 7

If you are interested you can read more at Announcing Entity Framework Core EF Core 5.0 Preview 7

更新 (07/06/2020)

UPDATE (07/06/2020)

Microsoft 发布了一个关于 Blazor(两种型号)的有趣的新视频和实体框架核心.请看19:20,他们在讨论如何用EFCore管理并发问题

Microsoft released a new interesting video about Blazor (both models) and Entity Framework Core. Please take a look at 19:20, they are talking about how to manage concurrency problem with EFCore

我向 Daniel Roth BlazorDeskShow - 2:24:20 询问了这个问题以及它设计上似乎是 Blazor 服务器端 问题.DbContext 默认生命周期设置为 Scoped.因此,如果您在同一页面中至少有两个组件尝试执行 async 查询,那么我们将遇到异常:

I asked Daniel Roth BlazorDeskShow - 2:24:20 about this problem and it seems to be a Blazor Server-Side problem by design.DbContext default lifetime is set to Scoped. So if you have at least two components in the same page which are trying to execute an async query then we will encounter the exception:

InvalidOperationException:在上一个操作完成之前,在此上下文中启动了第二个操作.不保证任何实例成员都是线程安全的.

关于这个问题有两个解决方法:

  • (A) 将 DbContext 的生命周期设置为 Transient
services.AddDbContext<ApplicationDbContext>(opt =>
    opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);

  • (B) 正如 Carl Franklin 建议的那样(在我的问题之后):创建一个带有静态方法的单例服务,该方法返回 DbContext 的新实例.
    • (B) as Carl Franklin suggested (after my question): create a singleton service with a static method which returns a new instance of DbContext.
    • 无论如何,每个解决方案都有效,因为它们创建了 DbContext 的新实例.

      anyway, each solution works because they create a new instance of DbContext.

      我的问题与 DbContext 并不严格相关,而是与 UserManager 相关,它具有 Scoped 生命周期.将 DbContext 的生命周期设置为 Transient 并没有解决我的问题,因为 ASP.NET Core 在我第一次打开会话时创建了一个 UserManager 的新实例,它直到我不关闭它为止.此 UserManager 位于同一页面上的两个组件内.然后我们遇到了之前描述的相同问题:

      My problem wasn't strictly related to DbContext but with UserManager<TUser> which has a Scoped lifetime. Set DbContext's lifetime to Transient didn't solve my problem because ASP.NET Core creates a new instance of UserManager<TUser> when I open the session for the first time and it lives until I don't close it. This UserManager<TUser> is inside two components on the same page. Then we have the same problem described before:

      • 拥有相同 UserManager 实例的两个组件,该实例包含一个瞬态 DbContext.
      • two components that own the same UserManager<TUser> instance which contains a transient DbContext.

      目前,我用另一种解决方法解决了这个问题:

      Currently, I solved this problem with another workaround:

      • I don't use UserManager<TUser> directly instead, I create a new instance of it through IServiceProvider and then it works. I am still looking for a method to change the UserManager's lifetime instead of using IServiceProvider.

      tips:注意服务的生命周期

      这是我学到的.不知道对不对.

      This is what I learned. I don't know if it is all correct or not.

      这篇关于使用 Entity Framework Core 的 Blazor 并发问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-05 18:23