英文:
I need help to find a good solution to inject DBContext into Repository Services
问题
我有一个新的Blazor项目,它与另一个API项目共享一个通用的Repository模块。Repository的构造函数需要一个DBContext
实例和一个AutoMapper
实例。我看到最佳实践建议我应该使用IDbContextFactory<MyDBContext>
。问题是我无法在服务注入时使用它,所以我想出了这种方法,目前正在工作,但我敢打赌我还没有意识到其中的一些缺点。所以任何帮助或建议将不胜感激。
var connectionString = configuration.GetConnectionString("PAADB") ?? throw new InvalidOperationException("Connection string 'PAADB' not found.");
services.AddDbContextFactory<MyDBContext>(options => options.UseSqlServer(connectionString), ServiceLifetime.Scoped);
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddScoped(ConfigurePersistenceService<IUserPersistenceService, UserPersistenceService>);
services.AddScoped(ConfigurePersistenceService<ICrmFirmsPersistenceService, CrmFirmsPersistenceService>);
services.AddScoped(ConfigurePersistenceService<ICrmAdvisorsPersistenceService, CrmAdvisorsPersistenceService>);
.......
static I ConfigurePersistenceService<I, T>(IServiceProvider provider) where T : class
{
var db = provider.GetRequiredService<IDbContextFactory<MyDBContext>>();
var autoMapper = provider.GetRequiredService<IMapper>();
return (I)Activator.CreateInstance(typeof(T), db.CreateDbContext(), autoMapper);
}//ConfigurePersistenceService - special way to pass db instance
Repository服务示例
public class CrmFirmsPersistenceService : PersistenceService<CrmFirm>, ICrmFirmsPersistenceService
{
private readonly IMapper _mapper;
public CrmFirmsPersistenceService(MyDBContext context, IMapper mapper) : base(context)
{
_mapper = mapper;
}
public async Task<(IEnumerable<CrmFirmDTO>, int)> GetFirmsWithPagination(int pageNumber, int pageSize)
{
var baseQuery = _context.CrmFirms;
var lstItems = await baseQuery.OrderByDescending(x => x.Id).Skip((pageNumber) * pageSize).Take(pageSize).ToListAsync();
var total = baseQuery.Count();
return (_mapper.Map<IEnumerable<CrmFirmDTO>>(lstItems), total);
}
}
有时我会收到的异常:
"A second operation was started on this context instance 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."
英文:
I have new Blazor project that is sharing a common Repository module with another API project. The Repository constructors services require a DBContext
instance and an AutoMapper
instance. I saw that the best practices are saying that I should use IDbContextFactory<MyDBContext>
. The problem is that I can't use it at the inject time into my services, so I came up with this approach that is working for now but I bet there are some downsides that I am not yet aware of them. So any help or advice would be appreciated.
var connectionString = configuration.GetConnectionString("PAADB") ?? throw new InvalidOperationException("Connection string 'PAADB' not found.");
services.AddDbContextFactory<MyDBContext>(options => options.UseSqlServer(connectionString), ServiceLifetime.Scoped);
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddScoped(ConfigurePersistenceService<IUserPersistenceService, UserPersistenceService>);
services.AddScoped(ConfigurePersistenceService<ICrmFirmsPersistenceService, CrmFirmsPersistenceService>);
services.AddScoped(ConfigurePersistenceService<ICrmAdvisorsPersistenceService, CrmAdvisorsPersistenceService>);
.......
static I ConfigurePersistenceService<I, T>(IServiceProvider provider) where T : class
{
var db = provider.GetRequiredService<IDbContextFactory<MyDBContext>>();
var autoMapper = provider.GetRequiredService<IMapper>();
return (I)Activator.CreateInstance(typeof(T), db.CreateDbContext(), autoMapper);
}//ConfigurePersistenceService - special way to pass db instance
Repository service example
public class CrmFirmsPersistenceService : PersistenceService<CrmFirm>, ICrmFirmsPersistenceService
{
private readonly IMapper _mapper;
public CrmFirmsPersistenceService(MyDBContext context, IMapper mapper) : base(context)
{
_mapper = mapper;
}
public async Task<(IEnumerable<CrmFirmDTO>, int)> GetFirmsWithPagination(int pageNumber, int pageSize)
{
var baseQuery = _context.CrmFirms;
var lstItems = await baseQuery.OrderByDescending(x => x.Id).Skip((pageNumber) * pageSize).Take(pageSize).ToListAsync();
var total = baseQuery.Count();
return (_mapper.Map<IEnumerable<CrmFirmDTO>>(lstItems), total);
}
}
}
Exceptions that I get from time to time:
A second operation was started on this context instance 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.
答案1
得分: 0
I saw that the best practices are saying that I should use IDbContextFactory<MyDBContext>
这不是一个“最佳实践”,事实上,在这种情况下,这是一个问题。
The actual problem that needs solving is that the DI scope in a Blazor Server application is the entire user session. There are many options available if we need scope semantics, and a specialized factory is only one of them. In fact, DbContextFactory is only meant to generate user-managed DbContext instances. It's not meant to control the lifetime of wrappers or dependent objects.
真正需要解决的问题是Blazor Server应用程序中的DI范围是整个用户会话。有许多可用选项,如果我们需要范围语义,专用工厂只是其中之一。实际上,DbContextFactory只是用来生成由用户管理的DbContext实例的。它不用于控制包装器或依赖对象的生命周期。
DbContextFactory - 用户管理范围
DbContextFactory用于在需要明确管理DbContext范围的情况下使用,例如在函数级别。它只创建一个DbContext,而不会将其释放。这在文档示例中是明显的:
@inject IDbContextFactory<ContactContext> DbFactory
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
...
}
这使其不适合CrmFirmsPersistenceService
。至少,服务的调用者应显式释放服务,以便它能释放DbContext实例。
组件范围
解决问题的一种方法是将服务范围限制为组件的生命周期。这可以通过继承自OwningComponentBase来实现。
在此文档示例中,AppDbContext
的范围限定为组件的生命周期。它可以通过Service
属性访问:
@page "/users"
@attribute [Authorize]
@inherits OwningComponentBase<AppDbContext>
<h1>Users (@Service.Users.Count())</h1>
<ul>
@foreach (var user in Service.Users)
{
<li>@user.UserName</li>
}
</ul>
在这个问题的情况下,这可以在不需要上下文工厂或激活器的情况下工作,只需将DbContext和CrmFirmsPersistenceService等注册为范围服务。DI配置可能如下:
services.AddDbContext<MyDBContext>(options => options.UseSqlServer(connectionString));
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddScoped<ICrmFirmsPersistenceService, CrmFirmsPersistenceService>();
组件本身需要继承自OwningComponentBase<T>
。
@page "/firms"
@inherits OwningComponentBase<ICrmFirmsPersistenceService>
<h1>Firms (@page.count)</h1>
<ul>
@foreach (var firm in page.firms)
{
...
}
</ul>
@code {
(IEnumerable<CrmFirmDTO> firms, int count) page = null!;
protected override async Task OnInitializedAsync()
{
page = await Service.GetFirmsWithPagination();
}
}
显式范围
当我们需要定义更精细的范围时,需要使用IServiceProvider.CreateScope来显式创建它,并使用该范围创建服务。这就是单例托管服务如何使用范围服务的方式。
为此,需要将IServiceProvider
作为组件的依赖项添加:
@page "/firms"
@inject IServiceProvider Services
然后,当需要创建ICrmFirmsPersistenceService
范围实例时,需要通过显式范围进行操作:
using var scope=Services.CreateScope();
var firmsService=scope.GetRequiredService<ICrmFirmsPersistenceService>();
...
当该范围被释放时,通过它创建的任何范围服务也将被释放,包括CrmFirmsPersistenceService
和其DbContext。
英文:
> I saw that the best practices are saying that I should use IDbContextFactory<MyDBContext>
That's not a "best practice", in fact, in this case, it's a problem.
The actual problem that needs solving is that the DI scope in a Blazor Server application is the entire user session. There are many options available if we need scope semantics, and a specialized factory is only one of them. In fact, DbContextFactory is only meant to generate user-managed DbContext instances. It's not meant to control the lifetime of wrappers or dependent objects.
DbContextFactory - user managed scope
A DbContextFactory is meant to be used when we want to explicitly manage the scope of a DbContext only, eg at the function level. It only creates a DbContext, it doesn't dispose it. This is evident in the documentation examples :
@inject IDbContextFactory<ContactContext> DbFactory
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
...
}
This makes it a bad fit for CrmFirmsPersistenceService
. At the very least, the service's callers should explicitly dispose the service so it can dispose the DbContext instance in turn.
Component scope
One way to solve the problem is to limit the service scope to a component's lifetime. This can be done by inheriting from OwningComponentBase.
In this documentation example, AppDbContext
is scoped to the component's lifetime. It's accessible through the Service
property :
@page "/users"
@attribute [Authorize]
@inherits OwningComponentBase<AppDbContext>
<h1>Users (@Service.Users.Count())</h1>
<ul>
@foreach (var user in Service.Users)
{
<li>@user.UserName</li>
}
</ul>
In the question's case, this would work without requiring a context factory or activator, simply by registering the DbContexts and CrmFirmsPersistenceService et al as scoped services.
The DI configuration could be :
services.AddDbContext<MyDBContext>(options => options.UseSqlServer(connectionString));
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddScoped<ICrmFirmsPersistenceService, CrmFirmsPersistenceService>();
The component itself would need to inherit from OwningComponentBase<T>
@page "/firms"
@inherits OwningComponentBase<ICrmFirmsPersistenceService>
<h1>Firms (@page.count)</h1>
<ul>
@foreach (var firm in page.firms)
{
...
}
</ul>
@code {
(IEnumerable<CrmFirmDTO> firms, int count) page = null!;
protected override async Task OnInitializedAsync()
{
page = await Service.GetFirmsWithPagination();
}
Explicit Scope
When we want to define a finer scope, we need to create it explicitly with IServiceProvider.CreateScope and create services using that scope. That's how singleton Hosted Services can consume scoped services.
To do this, we need to add IServiceProvider
as a dependency to the component:
@page "/firms"
@inject IServiceProvider Services
Then, when we need to create an ICrmFirmsPersistenceService
scoped instance, we need to do so through an explicit scope:
using var scope=Services.CreateScope();
var firmsService=scope.GetRequiredService<ICrmFirmsPersistenceService>();
...
When that scope gets disposed, any scoped serviced created through it will also get disposed, including the CrmFirmsPersistenceService
and its DbContext
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论