【问题标题】:Blazor Parent and child OnInitializedAsync accessing DB context at same timeBlazor 父子 OnInitializedAsync 同时访问数据库上下文
【发布时间】:2021-07-21 13:11:59
【问题描述】:

父母和孩子都必须访问数据库上下文才能获取他们的特定数据,下面是他们的代码。

家长:

[Inject]
private IProductsService ProductService { get; set; }
private IEnumerable<ProductModel> ProdList;      
private bool FiltersAreVisible = false;

protected override async Task OnInitializedAsync()
{
  ProdList = await ProductService.GetObjects(null);            
}

孩子:

[Parameter]
public IEnumerable<ProductModel> ProdList { get; set; }
[Parameter]
public EventCallback<IEnumerable<ProductModel>> ProdListChanged { get; set; } 
[Inject]
private IRepositoryService<ProdBusinessAreaModel> ProdBAreasService { get; set; }
[Inject]
private IRepositoryService<ProdRangeModel> ProdRangesService { get; set; }
[Inject]
private IRepositoryService<ProdTypeModel> ProdTypesService { get; set; }
[Inject]
private IProductsService ProductService { get; set; }        
private ProductFilterModel Filter { get; set; } = new ProductFilterModel();
private EditContext EditContext;
private IEnumerable<ProdBusinessAreaModel> ProdBAreas;
private IEnumerable<ProdRangeModel> ProdRanges;
private IEnumerable<ProdTypeModel> ProdTypes;

protected override async Task OnInitializedAsync()
{
  EditContext = new EditContext(Filter);            
  EditContext.OnFieldChanged += OnFieldChanged;

  ProdBAreas = await ProdBAreasService.GetObjects();
  ProdRanges = await ProdRangesService.GetObjects();
  ProdTypes = await ProdTypesService.GetObjects();
}

这会引发以下异常: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.

使用断点我看到父级运行OnInitializedAsync,当到达ProdList = await ProductService.GetObjects(null); 时立即跳转到子级OnInitializedAsync

我通过从父级发出所有请求然后传递给子级来解决它,但我想知道是否有更好的方法来做到这一点,让子级能够获取自己的数据,当然也不会使数据库上下文瞬态。 .

问候

【问题讨论】:

  • 您是如何注册服务的?它们是否以某种方式限定范围?
  • 我的服务都是范围的
  • 这可能非常浪费并导致问题。祝你好运。
  • @HenkHolterman 所有这些服务都通过依赖注入访问数据库上下文,因此它们不能设置为 sigleton,大多数是 Scoped
  • 单例对于 DbContext 来说是非常糟糕的。 Scoped 在 Blazor 中效果不太好,但您可以尝试。请注意,瞬态服务是最容易使用的,你不能对它们做任何错事。

标签: blazor parent-child dbcontext database-concurrency


【解决方案1】:

Blazor 没有方便的范围来管理数据库。解决此问题的方法是使用工厂(无需管理)并在每个方法中使用 using 块限定实际的 DbContext。

我们看不到您是如何实现 ProductService 的,但它应该是这样的

// inject a DbContextFactory and not the DbContext
public ProductService (IDbContextFactory<ProductDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

public Task<IEnumerable<ProductModel>> GetObjects()
{
   using var dbCtx = _contextFactory.CreateDbContext();

   // use dbCtx to return your results here
}

在你的 Startup 类中

services.AddDbContextFactory<ProductDbContext>(
    options => options.UseSqlServer(config.MyConnectionString));

您可以使用现有的任何 Db 配置。

【讨论】:

  • 顺便说一句,对于 Blazor Factory,您需要在使用它的每个组件中实现 IDisposable,否则即使在组件销毁后上下文仍将保留,因此它需要这种类型的管理
  • 不,每个方法都有一个 using。该服务不是 IDisposbale,可以注册为 Transient。
  • 组件(页面)的处置不够快或不够频繁。
  • 注册为 Trasient 是一个问题,因为您将失去对它的实体的跟踪,您传递模型的每个类都会在您创建多个上下文并在同一个组件中销毁它们的每个方法中使用表现不佳,工作就像一个 Trasient..
  • 是的,这里有问题,如果我不想要 Trasient 是有原因的
【解决方案2】:

您应该实现 DbContext 工厂,以防止两个或多个工作单元针对同一请求竞争相同资源的情况。请参阅下面的代码示例如何做到这一点。一般来说,您应该始终实现 DbContext 工厂......但是,从单个位置(例如,从您的父组件)检索您的数据,并以参数。更好的是,创建一个实现 State 和 Notify 模式的服务以向感兴趣的组件提供数据、通知它们更改以及通常管理和处理与数据相关的所有内容是一个好主意。大师 Steve Anderson 创建的 FlightFinder Blazor 应用程序示例就是一个很好的例子。但是,您应该跟随自己的心,按照自己的意愿编写代码。我只是指出推荐的模式。

这是您可以预览并适应您的应用的代码示例:

ContactContext.cs

/// <summary>
    /// Context for the contacts database.
    /// </summary>
    public class ContactContext : DbContext
    {
        /// <summary>
        /// Magic string.
        /// </summary>
        public static readonly string RowVersion = nameof(RowVersion);

        /// <summary>
        /// Magic strings.
        /// </summary>
        public static readonly string ContactsDb = nameof(ContactsDb).ToLower();

        /// <summary>
        /// Inject options.
        /// </summary>
        /// <param name="options">The <see cref="DbContextOptions{ContactContext}"/>
        /// for the context
        /// </param>
        public ContactContext(DbContextOptions<ContactContext> options)
            : base(options)
        {
            Debug.WriteLine($"{ContextId} context created.");
        }

        /// <summary>
        /// List of <see cref="Contact"/>.
        /// </summary>
        public DbSet<Contact> Contacts { get; set; }

        /// <summary>
        /// Define the model.
        /// </summary>
        /// <param name="modelBuilder">The <see cref="ModelBuilder"/>.</param>
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // this property isn't on the C# class
            // so we set it up as a "shadow" property and use it for concurrency
            modelBuilder.Entity<Contact>()
                .Property<byte[]>(RowVersion)
                .IsRowVersion();

            base.OnModelCreating(modelBuilder);
        }

        /// <summary>
        /// Dispose pattern.
        /// </summary>
        public override void Dispose()
        {
            Debug.WriteLine($"{ContextId} context disposed.");
            base.Dispose();
        }

        /// <summary>
        /// Dispose pattern.
        /// </summary>
        /// <returns>A <see cref="ValueTask"/></returns>
        public override ValueTask DisposeAsync()
        {
            Debug.WriteLine($"{ContextId} context disposed async.");
            return base.DisposeAsync();
        }
    } 

配置服务

 // register factory and configure the options
            #region snippet1
            services.AddDbContextFactory<ContactContext>(opt =>
                opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
                .EnableSensitiveDataLogging());
            #endregion 

将其注入组件的方法如下:

@inject IDbContextFactory<ContactContext> DbFactory

下面是如何使用它的代码示例:

using var context = DbFactory.CreateDbContext();

        // this just attaches
        context.Contacts.Add(Contact);

        try
        {
            await context.SaveChangesAsync();
            Success = true;
            Error = false;
            // ready for the next
            Contact = new Contact();
            Busy = false;
        }
        catch (Exception ex)
        {
            Success = false;
            Error = true;
            ErrorMessage = ex.Message;
            Busy = false;
        }

更新:

父级传递给子级数据并在洞范围内仅使用一个上下文与 DB 上下文工厂相比性能要好多少?

首先,无论如何你都应该实现 DbContext 工厂,对吧!?再一次,我不建议使用“父传递...孔范围”而不是实现 DbContext 工厂。在 Blazor 中,您必须实现 DbContext 工厂资源竞赛。好的。但也建议从一个位置公开您的数据:无论是服务还是父组件。在 Angular 和 Blazor 等框架中使用的组件模型中,数据通常是从父级到子级的下游流动。我敢肯定,您已经看到了许多这样做的代码示例,这就是您应该编码的方式。

【讨论】:

  • 父级传递给子级数据并在洞范围内仅使用一个上下文与 DB 上下文工厂相比性能好多少?
  • 我已添加更新以回应您的评论。请务必理解我在说什么,因为在我看来你误解了我的意思。
  • 我明白你的意思,我可以在不使用上下文工厂的情况下做一个完整的 blazor 应用程序,让父级获取所有数据然后传递给子级。我试图找出选择其中一个的理由..
  • 没有理由选择另一个,因为每个都为特定问题提供了解决方案,但在您的情况下,糟糕的代码设计和状态管理暴露了 DBContext 的问题。同样,您应该实施 DBContext 工厂作为预防措施,以防止由于在 Blazor 中使用 DBContext 对象而可能出现的不可预知的问题,但您应该正确处理状态,无论是使用服务还是将数据从父组件下游传输到子组件.
【解决方案3】:

欢迎来到异步世界。您有两个进程尝试使用相同的DbContext

解决办法是使用多个DbContexts通过DbContextFactory管理。

Here's the relevant Ms-Docs information.

相关部分是here - using-a-dbcontext-factory-eg-for-blazor

然后您可以执行以下操作:

public override async ValueTask<List<MyModel>> SelectAllRecordsAsync()
{
   var dbContext = this.DBContext.CreateDbContext();
   var list =  await dbContext
     .MyModelDbSet
     .ToListAsync() ?? new List<TRecord>();
  dbContext?.Dispose();
  return list;
}

服务上的 IDisposable 和 IAsyncDisposable

您需要非常小心地在服务上实现IDisposableIAsyncDisposable。 Scoped Services 容器创建任何 Transient 服务的实例,将引用传递给请求者并忘记它,让垃圾收集器在组件完成后清理它。但是,如果服务实现了IDisposableIAsyncDisposable,它会保留一个引用,但仅在服务容器本身被 Disposed 时(当用户会话结束时)调用 Dispose。因此,使用 DbContexts 的瞬态服务可能会导致严重的内存泄漏。

有一个解决方法(不是解决方案)使用OwningComponentBase&lt;T&gt; 而不是ComponentBase 的组件。这会为组件的生命周期创建一个服务容器,因此当组件超出范围时,Dispose 会运行。内存泄漏的可能性仍然存在,但寿命要短得多!

【讨论】:

  • 感谢您的回答,我有 3 个问题。怎么样:“请注意,以这种方式创建的 DbContext 实例不由应用程序的服务提供者管理,因此必须由应用程序处置。”这意味着我必须总是在 unig 之后处理上下文?并且每次我需要访问数据库时创建实例并不会降低它的性能,然后是我询问父级所有内容的解决方案?会不会和让 dbcontext Transient 一样(失去类之间的跟踪)?
  • 我已经更新了我的示例以包括处置。我从来没有使用过瞬态方法。使用 DBContextFactory 方法的工作单元有效并且似乎是公认的做法。编码的方式总是不止一种。
  • 阅读文档可以在组件中实现 IDisposable,当组件被销毁时,它会销毁其上下文,否则上下文将存在
  • 稍后回复 - 请参阅我关于服务的 IDisposableIAsyncDisposable 的更新答案。
猜你喜欢
  • 2014-12-02
  • 2021-10-27
  • 2016-12-07
  • 2013-04-08
  • 2021-10-21
  • 2015-07-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多