c# - 在处理复杂数据模型时使用 DbContextFactory(每个服务方法/操作都有一个新的 DbContext)
问题描述
更新:线程问题是由于 ApplicationDbContext 被注册为 Scoped 并且服务被注册为 Transient 引起的。将我的 ApplicationDbContext 注册为瞬态已解决了线程问题。但是:我不想失去工作单元和更改跟踪功能。相反,我现在将 ApplicationDbContext 保持为 Scoped,并通过使用 Semaphore 来防止同时调用来解决问题,正如我在此处的回答中所解释的那样:https ://stackoverflow.com/a/68486531/13678817
我的 Blazor Server 项目使用 EF Core,具有复杂的数据库模型(某些实体具有 5 级以上的子实体)。当用户从导航菜单导航到新组件时,相关实体会加载到 OnInitializedAsync 中(我在其中为每种实体类型注入一个服务)。每个服务在启动时都注册为 Transient。加载的实体在此组件及其子/嵌套组件中进行操作。
但是,当用户在组件之间导航而前一个组件的服务仍在加载实体(...第二个操作已经开始...)时,这种方法会导致线程问题(不同线程同时使用相同的 DbContext 实例)。
以下是导致此错误的简化原始代码。
组件 1:
@page : "/bases"
@using....
@inject IBasesService basesService
@inject IPeopleService peopleService
<h1>...//Code omitted for brevity
@code{
List<Bases> bases;
List<Person> people;
protected override async Task OnInitializedAsync()
{
bases = await basesService.GetBasesAndRelatedEntities();
people = await peopleService.GetPeopleAndRelatedEntities();
}
组件 2:
@page : "/people"
@using....
@inject IBasesService basesService
@inject IPeopleService peopleService
<h1>...//Code omitted for brevity
@code{
List<Person> people;
protected override async Task OnInitializedAsync()
{
people = await peopleService.GetPeopleAndRelatedEntities();
}
此外,所有服务都具有这种结构,并在启动时注册为瞬态:
我的基地服务:
public interface IBasesService
{
Task<List<Base>> Get();
Task<List<Base>> GetBasesAndRelatedEntities();
Task<Base> Get(Guid id);
Task<Base> Add(Base Base);
Task<Base> Update(Base Base);
Task<Base> Delete(Guid id);
void DetachEntity(Base Base);
}
public class BasesService : IBasesService
{
private readonly ApplicationDbContext _context;
public BasesService(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<Base>> GetBasesAndRelatedEntities()
{
return await _context.Bases.Include(a => a.People).ToListAsync();
}
//...code ommitted for brevity
DbContext 注册如下:
services.AddDbContextFactory<ApplicationDbContext>(b =>
b.UseSqlServer(
Configuration.GetConnectionString("MyDbConnection"), sqlServerOptionsAction: sqlOptions =>
{ //Updated according to https://dev-squared.com/2018/07/03/tips-for-improving-entity-framework-core-performance-with-azure-sql-databases/
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorNumbersToAdd: null);
}
));
/bases
现在,我的用户可以/people
使用导航菜单进行切换。如果他们快速执行此操作,则await peopleService.GetPeopleAndRelatedEntities();
在前一个组件完成之前调用 另一个组件await peopleService.GetPeopleAndRelatedEntities();
,这会导致如下错误:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [**Sensitive DB statement ommitted**]
fail: Microsoft.EntityFrameworkCore.Query[10100]
An exception occurred while iterating over the results of a query for context type '[**ommitted**].Data.ApplicationDbContext'.
System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
dbug: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[12]
Message received. Type: Binary, size: 422, EndOfMessage: True.
dbug: Microsoft.Azure.SignalR.ServiceConnection[16]
Received 422 bytes from service 468a12a0...
warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100]
Unhandled exception rendering component: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at [**path to service ommitted**]...cs:line 35
at [**path to component ommitted**].razor:line 77
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
Unhandled exception in circuit 'cvOyWXdG_oikG_YJe2ehrsHsI3VQDJw2U8YIySmroTM'.
System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at [**path to service ommitted**].cs:line 35
at [**path to component ommitted**].razor:line 77
我通读了有关此主题的所有 Stack Overflow 和 MS 文档,并根据推荐的使用 DbContextFactory 的方法调整了我的项目:
假设我有以下数据库模型:
每个 AlphaObject 都有很多 BetaObjects,其中有很多 CharlieObjects。每个CharlieObject 有1 个BetaObject,每个BetaObject 有1 个AlphaObject。
在每次操作之前,我调整了所有服务以使用 DbContextFactory 创建一个新的 DbContext:
public AlphaObjectsService(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<List<AlphaObject>> GetAlphaObjectAndRelatedEntities()
{
using (var _context = _contextFactory.CreateDbContext())
return await _context.AlphaObjects.Include(a => a.BetaObjects).ThenInclude(b => b.CharlieObjects).ToListAsync();
}
之前,我会加载一个List<AlphaObject> alphaObjects
,并在服务中包含所有相关的 BetaObject 实体(以及它们相关的 CharlieObject 实体),然后我可以稍后加载一个列表List<BetaObject> betaObjects
,并且在不明确包括其相关的 AlphaObject 或 CharlieObjects 的情况下,它已经是如果之前加载过,则加载。
现在,当使用“每个操作的 DbContext”时 - 如果我没有再次显式加载它们,我的许多相关实体都是空的。我还担心操纵实体及其相关实体,并更新所有这些更改,而没有带有更改跟踪的 DbContext 的正常生命周期。EF Core 文档指出 DbContext 的正常生命周期应该是:
- 创建 DbContext 实例
- 跟踪一些实体
- 对实体进行一些更改
- 调用 SaveChanges 更新数据库
- 释放 DbContext 实例。
为了解决我的线程(相同 DbContext 的并发访问)错误,但继续以 EF Core 的预期使用方式操作我的实体:
我是否应该将 DbContext 的生命周期延长到从数据库加载实体的主要组件的生命周期?
这样,在加载一个实体及其所有相关实体后,我不需要为另一种实体类型加载所有已加载的实体。我还将获得其他好处,例如在组件的生命周期内进行更改跟踪,并且在调用时保存对被跟踪实体的所有更改
context.SaveChangesAsync()
。但是,我需要手动处理使用 DbContextFacoty 创建的每个上下文。根据 MS Docs,似乎我必须直接从组件访问数据库才能实现 IDisposable(我需要直接在我的组件中创建上下文,而不是在服务中 - 与 MSDocs 示例应用程序一样:https ://docs.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-5.0)。直接从组件内创建和访问 DbContext 真的是最佳/推荐吗?否则,不是实现 IDisposable,而是使用OwningComponentBase 具有完全相同的能力来在组件的生命周期结束后处理上下文,除非我可以使用我现有的服务吗?
我可以在每次服务操作后继续处置我的新 DbContext -
using (var _context = _contextFactory.CreateDbContext())
吗?那么,我是否必须简单地确保每次加载不同的实体类型时,我也应该再次加载所有必需的相关实体?即
return await context.AlphaObject.Include(a => a.BetaObject).ThenInclude(b => b.CharlieObject).ToListAsync();
,在加载 CharlieObject 列表时,我应该再次明确包括 BetaObject 和 AlphaObject?我是否仍然能够在我的子组件中对 AlphaObject 及其相关的 Beta- 和 CharlieObjects 进行更改,然后在完成所有更改后,进行context.Entry(AlphaObject).State = EntityState.Modified
和调用context.SaveChangesAsync()
还会更新对 BetaObjects 和 CharlieObjects 所做的更改阿尔法对象?或者是否需要将每个实体的状态更改为 EntityState.Modified?
简而言之,我很想了解确保在单个 DbContext 生命周期之外工作时正确加载(并操作和更新)相关实体的正确方法,因为这似乎是推荐的方法。与此同时,我将继续调整我的项目以使用每个服务操作的新上下文,并继续“边学习边学习”。当我了解更多信息时,我会更新这个问题。
解决方案
推荐阅读
- python - 将大型 json 文件读入 pandas 数据帧
- xamarin - Xamarin Forms Image 未显示
- c# - 在 C# 中将 byte[] 转换为类的实例
- c++ - 从多个 C/C++ 线程调用 Haskell
- reactjs - 设置具有多个入口点的 webpack 配置的最佳方法
- sql - PostgreSQL 11:使用 json_object_agg() 的多个键值对
- javascript - 如何在数组中添加消息?
- html - 单击列后排序图标更改位置
- abap - 如何从 DB 表中选择 LRAW?
- asp.net-mvc - 将 Azure AD 身份验证添加到现有 .Net MVC 应用程序:未验证 id_token