我需要帮助找到一个好的解决方案,将DBContext注入到存储库服务中。

huangapple go评论66阅读模式
英文:

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&lt;ContactContext&gt; DbFactory

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    ...
}

这使其不适合CrmFirmsPersistenceService。至少,服务的调用者应显式释放服务,以便它能释放DbContext实例。

组件范围

解决问题的一种方法是将服务范围限制为组件的生命周期。这可以通过继承自OwningComponentBase来实现。

在此文档示例中,AppDbContext的范围限定为组件的生命周期。它可以通过Service属性访问:

@page &quot;/users&quot;
@attribute [Authorize]
@inherits OwningComponentBase&lt;AppDbContext&gt;

&lt;h1&gt;Users (@Service.Users.Count())&lt;/h1&gt;

&lt;ul&gt;
    @foreach (var user in Service.Users)
    {
        &lt;li&gt;@user.UserName&lt;/li&gt;
    }
&lt;/ul&gt;

在这个问题的情况下,这可以在不需要上下文工厂或激活器的情况下工作,只需将DbContext和CrmFirmsPersistenceService等注册为范围服务。DI配置可能如下:

services.AddDbContext&lt;MyDBContext&gt;(options =&gt; options.UseSqlServer(connectionString));
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddScoped&lt;ICrmFirmsPersistenceService, CrmFirmsPersistenceService&gt;();

组件本身需要继承自OwningComponentBase&lt;T&gt;

@page &quot;/firms&quot;
@inherits OwningComponentBase&lt;ICrmFirmsPersistenceService&gt;

&lt;h1&gt;Firms (@page.count)&lt;/h1&gt;

&lt;ul&gt;
    @foreach (var firm in page.firms)
    {
        ...
    }
&lt;/ul&gt;

@code {

    (IEnumerable&lt;CrmFirmDTO&gt; firms, int count) page = null!;
    protected override async Task OnInitializedAsync()
    {
        page = await Service.GetFirmsWithPagination();
    }
}

显式范围

当我们需要定义更精细的范围时,需要使用IServiceProvider.CreateScope来显式创建它,并使用该范围创建服务。这就是单例托管服务如何使用范围服务的方式。

为此,需要将IServiceProvider作为组件的依赖项添加:

@page &quot;/firms&quot;
@inject IServiceProvider Services

然后,当需要创建ICrmFirmsPersistenceService范围实例时,需要通过显式范围进行操作:

using var scope=Services.CreateScope();
var firmsService=scope.GetRequiredService&lt;ICrmFirmsPersistenceService&gt;();
...

当该范围被释放时,通过它创建的任何范围服务也将被释放,包括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&lt;ContactContext&gt; 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 &quot;/users&quot;
@attribute [Authorize]
@inherits OwningComponentBase&lt;AppDbContext&gt;

&lt;h1&gt;Users (@Service.Users.Count())&lt;/h1&gt;

&lt;ul&gt;
    @foreach (var user in Service.Users)
    {
        &lt;li&gt;@user.UserName&lt;/li&gt;
    }
&lt;/ul&gt;

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&lt;MyDBContext&gt;(options =&gt; options.UseSqlServer(connectionString));
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddScoped&lt;ICrmFirmsPersistenceService, CrmFirmsPersistenceService&gt;();

The component itself would need to inherit from OwningComponentBase&lt;T&gt;

@page &quot;/firms&quot;
@inherits OwningComponentBase&lt;ICrmFirmsPersistenceService&gt;

&lt;h1&gt;Firms (@page.count)&lt;/h1&gt;

&lt;ul&gt;
    @foreach (var firm in page.firms)
    {
        ...
    }
&lt;/ul&gt;

@code {

    (IEnumerable&lt;CrmFirmDTO&gt; 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 &quot;/firms&quot;
@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&lt;ICrmFirmsPersistenceService&gt;();
...

When that scope gets disposed, any scoped serviced created through it will also get disposed, including the CrmFirmsPersistenceService and its DbContext

huangapple
  • 本文由 发表于 2023年2月8日 18:22:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/75384356.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定