问题描述
父母和孩子都必须访问数据库上下文才能获得他们的具体数据,下面是他们的代码。
家长:
[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
您需要非常小心地对服务实施 IDisposable
或 IAsyncDisposable
。 Scoped Services 容器创建任何 Transient 服务的实例,将引用传递给请求者并忘记它,当组件完成它时让垃圾收集器清理它。但是,如果服务实现了 IDisposable
或 IAsyncDisposable
,它会保留一个引用,但仅在服务容器本身获得 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));
您可以使用您现在拥有的任何数据库配置。