asp.net-core - 使用 Entity Framework Core 的 Blazor 并发问题
问题描述
我的目标
我想创建一个新的 IdentityUser 并显示已通过同一 Blazor 页面创建的所有用户。这个页面有:
- 通过您的表单将创建一个 IdentityUser
- 第三方的网格组件 (DevExpress Blazor DxDataGrid),显示所有使用 UserManager.Users 属性的用户。该组件接受 IQueryable 作为数据源。
问题
当我通过表单(1)创建新用户时,会出现以下并发错误:
InvalidOperationException:在前一个操作完成之前在此上下文上启动了第二个操作。不保证任何实例成员都是线程安全的。
我认为问题与CreateAsync(IdentityUser user)和UserManager.Users指的是同一个 DbContext
该问题与第三方组件无关,因为我用一个简单的列表替换了相同的问题。
重现问题的步骤
- 使用身份验证创建新的 Blazor 服务器端项目
使用以下代码更改 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()}" }); } }
我注意到了什么
- 如果我将实体框架提供程序从 SqlServer 更改为 Sqlite,则该错误将永远不会显示。
系统信息
- ASP.NET Core 3.1.0 Blazor 服务器端
- 基于 SqlServer 提供程序的 Entity Framework Core 3.1.0
我已经看到的
- Blazor 在前一个操作完成之前在此上下文上启动了第二个操作:建议的解决方案对我不起作用,因为即使我将 DbContext 范围从Scoped更改为Transient我仍然使用相同的 UserManager 实例,并且它包含相同的实例数据库上下文
- StackOverflow 上的其他人建议为每个请求创建一个新的 DbContext 实例。我不喜欢这个解决方案,因为它违反了依赖注入原则。无论如何,我无法应用此解决方案,因为 DbContext 包装在 UserManager 中
- 创建 DbContext 的生成器:此解决方案与前一个解决方案非常相似。
- 将 Entity Framework Core 与 Blazor 一起使用
为什么我要使用 IQueryable
我想将 IQueryable 作为第三方组件的数据源传递,因为它可以将分页和过滤直接应用于查询。此外,IQueryable 对 CUD 操作很敏感。
解决方案
更新(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
.
目前,我用另一种解决方法解决了这个问题:
- 我不
UserManager<TUser>
直接使用,而是通过它创建一个新实例,IServiceProvider
然后它就可以工作了。我仍在寻找一种方法来更改 UserManager 的生命周期,而不是使用IServiceProvider
.
提示:注意服务的生命周期
这是我学到的。我不知道这一切是否正确。
推荐阅读
- c# - 为什么将文件上传到 Slack 的两种(几乎)相似的方法(使用 c#)会导致不同的结果?
- c# - 如何在不丢失 CefSharp 浏览器的情况下最小化和最大化 WinForms 表单?
- filehelpers - FileHelpers 问题 - 如何在没有换行符或分隔符的情况下读取固定长度文件
- css - 如何在 .NET Framework 项目中禁用 SCSS 编译文件
- html5-canvas - Canvas 中奇怪的视图框字体大小调整行为
- c# - 在 Visual Studio 2019 中重新启用标题栏
- javascript - 如何使用 JavaScript 显示图像和文本搜索结果?
- java - Spring boot Jsp / Tag在不同模块中
- jquery - Jssor 是否可以在滑块上放置一个暂停按钮并播放?
- spring - Spring Boot 执行器端点未通过 HTTP 公开