Blazor 父子 OnInitializedAsync 同时访问数据库上下文

问题描述

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

家长:

[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

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

问候

解决方法

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

解决方案是使用通过 DbContext 管理的多个 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。因此,对 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;
        }

更新:

父级传递给子级数据并通过孔范围仅使用一个上下文比 DB 上下文工厂的性能好多少?

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

,

Blazor 没有方便的 Scopes 来管理数据库。解决此问题的方法是使用工厂(无需管理)并使用 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
}

在你的创业班

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

您可以使用您现在拥有的任何数据库配置。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...