如果需要同时批量处理和异步执行事务,是否应将EF6 DbContext作为作用域或临时注入?

时间:2019-08-08 20:00:10

标签: c# entity-framework dependency-injection

大约2年前,我们进行了从ADO.net到Entity Framework 6的更改。最初,我们只是在需要它们的地方实例化了DbContexts。但是,在某些时候,我们开始了在解决方案中实施依赖注入的准备工作。这样,我们将DbContexts注入到MVC控制器构造函数中,然后使用DbContexts直接实例化必要的逻辑类。一段时间以来,这非常有效,因为我们有某些IRepository实现,使我们能够操纵多个存储库中的数十个实体,并通过单个SaveChanges调用保存它们。

但是,随着时间的流逝,我们已经开始采用更加纯粹的DI方法,在其中注入了所有新类(而不是实例化)。副作用是,我们开始从存储库转向使用EF作为解决方案中的核心存储库。这导致我们在应用程序中构建模块,以执行其工作单元并保存其更改。因此,我们没有使用和访问数十个存储库来执行操作,而是仅使用DbContext

最初,这很好,因为我们将DbContexts注入了作用域,并且功能未更改。但是,随着向更独立,更节省模块的方向发展,我们的新功能遇到了并发错误。通过将DbContexts的DI配置切换到瞬态,我们设法解决了并发问题。这为每个自包含的模块提供了一个新的DbContext,他们能够执行和保存而不关心其他模块的工作。

但是,不幸的是,将DbContexts切换到瞬态有一个副作用,即无法将我们的旧模块切换到我们的DI容器,因为它们依赖于所有设备上的单个共享DbContext他们注入的依赖项。

所以我的主要难题是我们应该将DbContexts设为作用域还是瞬态。如果我们确实确定了作用域,那么如何编写新模块以使它们可以并行执行?而且,如果我们决定使用过渡,那么如何在仍在开发和使用的数十个旧类中保留功能?


范围

优点

    每个请求
  • 单个DbContext。不用担心在不同的上下文中跟踪实体,并且可以批量保存。
  • 旧版代码无需进行任何重大更改即可切换为DI。

缺点

  • 不相关的任务不能在相同的上下文中同时执行。
  • 开发人员必须不断了解当前上下文的状态。他们需要警惕其他使用相同上下文的类的副作用。
  • System.NotSupportedException: 'A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.'在并发操作期间抛出。

暂时

优点

  • 每堂课新增DbContext。在上下文中执行大多数操作时,无需担心锁定上下文。
  • 模块变得自成体系,您无需担心其他类的副作用。

缺点

  • 从一个上下文中接收实体并尝试在其他上下文实例中使用它会导致错误。
  • 无法在共享同一上下文的多个不同类之间执行批处理操作。

这是一个演示算法,可为作用域上下文强制并发错误。它为瞬态注入提供了一个可能的用例。

// Logic Class
public class DemoEmrSaver
{
    private readonly DbContext_dbContext;

    public DemoEmrSaver(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public Task CreateEmrs(int number)
    {
        Contract.Assert(number > 0);
        for (var i = 0; i < number; i++)
            CreateEmr();

        return _dbContext.SaveChangesAsync();
    }

    private void CreateEmr()
    {
        var emr = new EMR
        {
            Name = Guid.NewGuid().ToString()
        };

        _dbContext.EMRs.Add(emr);
    }
}

// In a controller
public async Task<IActionResult> TestAsync()
{
    // in reality, this would be two different services.
    var emrSaver1 = new DemoEmrSaver(_dbContext);
    var emrSaver2 = new DemoEmrSaver(_dbContext);

    await Task.WhenAll(emrSaver1.CreateEmrs(5), emrSaver2.CreateEmrs(5));

    return Json(true);
}

这是旧服务通常如何运行的演示

public class DemoEmrSaver
{
    private readonly DbContext _dbContext;

    public DemoEmrSaver(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void CreateEmrs(int number)
    {
        Contract.Assert(number > 0);
        for (var i = 0; i < number; i++)
            CreateEmr();
    }
    private void CreateEmr()
    {
        var emr = new EMR
        {
            Name = Guid.NewGuid().ToString()
        };

        _dbContext.EMRs.Add(emr);
    }
}

// controller action
public async Task<IActionResult> TestAsync()
{
    var emrSaver1 = new DemoEmrSaver(_dbContext);
    var emrSaver2 = new DemoEmrSaver(_dbContext);

    emrSaver1.CreateEmrs(5);
    emrSaver2.CreateEmrs(5);

    await _catcContext.SaveChangesAsync();

    return Json(true);
}

是否有某种中间立场,不需要对旧代码进行大修,但仍然可以使我的新模块以简单的方式定义和使用(例如,避免通过Func进入每个构造函数的某种排序以获取新实例,而不必在我需要的任何地方专门要求一个新的DbContext

也许也很重要,我正在使用Microsoft.Extensions.DependencyInjection名称空间中的.Net Core DI容器。

1 个答案:

答案 0 :(得分:1)

为什么遇到这种困难,为什么不使用人工示波器呢?

例如,我们的代码库中有一些后台服务,当它们在普通的AspNet核心Web应用程序中使用时(如您所说,上下文绑定到请求),但是对于我们的控制台应用程序,我们没有范围的概念,所以我们必须自己定义它。

要创建人为的作用域,只需插入IServiceScopeFactory,然后,内部的所有内容都将使用新的分离上下文。

public class SchedulerService
{
    private readonly IServiceScopeFactory _scopeService;

    public SchedulerService(IServiceScopeFactory scopeService)
    {
        _scopeService = scopeService;
    }

    public void EnqueueOrder(Guid? recurrentId)
    {
        // Everything you ask here will be created as if was a new scope,
        // like a request in aspnet core web apps
        using (var scope = _scopeService.CreateScope())
        {
            var recurrencyService = scope.ServiceProvider.GetRequiredService<IRecurrencyService>();
            // This service, and their injected services (like the context)
            // will be created as if was the same scope
            recurrencyService.ProcessScheduledOrder(recurrentId);
        }
    }
}

这样,您可以控制作用域服务的生存期,从而帮助您在该块内共享相同的上下文。

我建议以这种方式只创建一个服务,然后在服务程序中按正常方式进行所有操作,这样您的代码将保持整洁并易于阅读,因此,请像下面的示例一样进行操作:

using (var scope = _scopeService.CreateScope())
{
    var recurrencyService = scope.ServiceProvider.GetRequiredService<IRecurrencyService>();
    // In this service you can do everything and is
    // contained in the same service
    recurrencyService.ProcessScheduledOrder(recurrentId);
}

请不要在用法中添加复杂的代码,例如

using (var scope = _scopeService.CreateScope())
{
    var recurrencyService = scope.ServiceProvider.GetRequiredService<IRecurrencyService>();
    var otherService= scope.ServiceProvider.GetRequiredService<OtherService>();
    var moreServices = scope.ServiceProvider.GetRequiredService<MoreServices>();

    var something = recurrencyService.SomeCall();
    var pleaseDoNotMakeComplexLogicInsideTheUsing = otherService.OtherMethod(something);
    ...
}

编辑

  

我对这种方法的担心是它正在应用服务定位器   模式,而且我经常看到它被当作反模式   DI很重要

一种反模式是将其用作正常工作,但我建议仅在一部分中进行介绍,DI的功能存在局限性和局限性,可以帮助您解决问题。

例如,属性注入(没有构造函数注入)也是一种代码味道,但是它并没有被框架禁止或删除,因为在某些情况下,这是唯一的解决方案,或者是最简单的解决方案,而保持简单则更加比保留所有良好做法重要(即使最佳做法也不是白人或黑人,有时您必须在遵循一个或其他原则之间进行权衡)。

我的解决方案应该放在程序的一部分中,而不是针对所有内容,这就是为什么我建议仅创建一个服务,然后从那里创建所有服务的原因,您不能使用构造函数注入来破坏作用域的生命周期,因此IServiceScopeFactory就是为此而存在。

当然,它不是用于一般用途,而是用于解决像您这样的生命周期问题。

如果您担心calling GetService<SomeClass>,可以创建一个抽象来保持代码干净,例如,我创建了以下常规服务:

public class ScopedExecutor
{
    private readonly IServiceScopeFactory _serviceScopeFactory;
    private readonly ILogger<ScopedExecutor> _logger;

    public ScopedExecutor(
        IServiceScopeFactory serviceScopeFactory,
        ILogger<ScopedExecutor> logger)
    {
        _serviceScopeFactory = serviceScopeFactory;
        _logger = logger;
    }

    public async Task<T> ScopedAction<T>(Func<IServiceProvider, Task<T>> action)
    {
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            return await action(scope.ServiceProvider);
        }
    }

    public async Task ScopedAction(Func<IServiceProvider, Task> action)
    {
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            await action(scope.ServiceProvider);
        }
    }
}

然后我有这个额外的层(您可以在与上一个相同的类中进行设置)

public class ScopedExecutorService<TService>
{
    private readonly ScopedExecutor _scopedExecutor;

    public ScopedExecutorService(
        ScopedExecutor scopedExecutor)
    {
        _scopedExecutor = scopedExecutor;
    }

    public Task<T> ScopedActionService<T>(Func<TService, Task<T>> action)
    {
        return _scopedExecutor.ScopedAction(serviceProvider =>
            action(
                serviceProvider
                    .GetRequiredService<TService>()
            )
        );
    }
}

现在,在需要将服务作为单独的上下文的地方,可以使用类似的内容

public class IvrRetrieveBillHistoryListFinancingGrpcImpl : IvrRetrieveBillHistoryListFinancingService.IvrRetrieveBillHistoryListFinancingServiceBase
{
    private readonly GrpcExecutorService<IvrRetrieveBillHistoryListFinancingHttpClient> _grpcExecutorService;

    public IvrRetrieveBillHistoryListFinancingGrpcImpl(GrpcExecutorService<IvrRetrieveBillHistoryListFinancingHttpClient> grpcExecutorService)
    {
        _grpcExecutorService = grpcExecutorService;
    }

    public override async Task<RetrieveBillHistoryListFinancingResponse> RetrieveBillHistoryListFinancing(RetrieveBillHistoryListFinancingRequest retrieveBillHistoryListFinancingRequest, ServerCallContext context)
    {
        return await _grpcExecutorService
            .ScopedLoggingExceptionHttpActionService(async ivrRetrieveBillHistoryListFinancingHttpClient =>
                await ivrRetrieveBillHistoryListFinancingHttpClient
                    .RetrieveBillHistoryListFinancing(retrieveBillHistoryListFinancingRequest)
            );
    }
}

如您所见,业务代码中没有service.GetService被调用,仅在我们工具箱中的一个地方