Blazor Parent 和 child OnInitializedAsync 同时访问数据库上下文

Blazor Parent and child OnInitializedAsync accessing DB context at same time

parent 和 child 都必须访问数据库上下文才能获取它们的特定数据,下面是它们的代码。

Parent:

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

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

Child:

[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.

使用断点我看到 parent 运行 OnInitializedAsync 并且当到达 ProdList = await ProductService.GetObjects(null); 时立即跳转到 child OnInitializedAsync.

我通过从 parent 发出所有请求然后传递给 child 来解决它,但我想知道是否有更好的方法来做到这一点,让 child 有能力获取其自己的数据,当然无需使数据库上下文瞬态..

此致

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

解决方案是使用通过 DbContextFactory 管理的多个 DbContext

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 它会保留一个引用,但仅在服务容器本身被处置时(当用户会话结束时)才调用处置。因此,使用 DbContext 的瞬态服务可能会导致严重的内存泄漏。

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

您应该实施 DbContext 工厂,以防止出现同一请求的两个或多个工作单元竞争同一资源的情况。请参阅下面的代码示例如何执行此操作。一般来说,您应该始终实现 DbContext 工厂......但是,从单个位置(例如,从您的父组件)检索数据并将其以以下形式传递给其子组件是一种更好的代码设计参数。更妙的是,创建一个实现状态和通知模式的服务来向感兴趣的组件提供数据,通知它们更改,并通常管理和处理与数据相关的一切是一个好主意。大师 Steve Anderson 创建的 FlightFinder Blazor App 示例就是一个很好的例子。但是,您应该遵循自己的内心,并按照自己的意愿编写代码。我只是指出推荐的模式。

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

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;
        }

更新:

Parent passing to child data and using only one context throught the hole scope is how much better performing then DB context Factory?

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

Blazor 没有方便的范围来管理 Db。解决此问题的方法是使用工厂(无需管理)并使用 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 class

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

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