首页 > 解决方案 > 使用 Entity Framework Core 的 Blazor 并发问题

问题描述

我的目标

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

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

问题

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

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

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

该问题与第三方组件无关,因为我用一个简单的列表替换了相同的问题。

重现问题的步骤

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

    @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()}" });
        }
    }
    

我注意到了什么

系统信息

我已经看到的

为什么我要使用 IQueryable

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

标签: asp.net-coreentity-framework-coreblazorblazor-server-side

解决方案


更新(2020 年 8 月 19 日)

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

更新(2020 年 7 月 22 日)

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

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

如果您有兴趣,可以在宣布 Entity Framework Core EF Core 5.0 Preview 7中阅读更多信息

更新(2020 年 7 月 6 日)

微软发布了一段关于 Blazor(两种型号)和 Entity Framework Core的有趣视频。请看 19:20,他们在讨论如何使用 EFCore 管理并发问题


一般解决方案

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

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

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

  • (A) 将 DbContext 的生命周期设置为Transient
services.AddDbContext<ApplicationDbContext>(opt =>
    opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
  • (B) 正如卡尔富兰克林建议的那样(在我的问题之后):创建一个带有静态方法的单例服务,该方法返回一个新的DbContext.

无论如何,每个解决方案都有效,因为它们创建了DbContext.

关于我的问题

我的问题与它没有严格的关系,DbContext但与UserManager<TUser>它有Scoped一生的关系。将 DbContext 的生命周期设置为Transient并没有解决我的问题,因为 ASP.NET CoreUserManager<TUser>在我第一次打开会话时创建了一个新实例,并且它一直存在到我不关闭它为止。这UserManager<TUser>是在同一页面上的两个组件内。然后我们遇到了之前描述的同样的问题:

  • 拥有同一个UserManager<TUser>实例的两个组件,其中包含一个瞬态DbContext.

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

提示:注意服务的生命周期

这是我学到的。我不知道这一切是否正确。


推荐阅读