首页 > 解决方案 > 在处理复杂数据模型时使用 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 的并发访问)错误,但继续以 EF Core 的预期使用方式操作我的实体:

  1. 我是否应该将 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 具有完全相同的能力来在组件的生命周期结束后处理上下文,除非我可以使用我现有的服务吗?

  2. 我可以在每次服务操作后继续处置我的新 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 生命周期之外工作时正确加载(并操作和更新)相关实体的正确方法,因为这似乎是推荐的方法。与此同时,我将继续调整我的项目以使用每个服务操作的新上下文,并继续“边学习边学习”。当我了解更多信息时,我会更新这个问题。

标签: c#entity-framework-coreblazor-server-side.net-5

解决方案


推荐阅读