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

标签 c# entity-framework dependency-injection

大约2年前,我们从ADO.net更改为Entity Framework6。最初,我们只是在需要它们的地方实例化了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容器。

最佳答案

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

例如,我们的代码库中有一些后台服务,当它们在普通的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,可以创建一个抽象来保持代码干净,例如,我创建了以下常规服务:

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)
            );
    }
}


如您所见,业务代码中没有调用calling GetService<SomeClass>,仅在我们工具箱中的一个位置

关于c# - 如果需要同时批量处理事务和异步执行事务,是否应该将EF6 DbContext作为作用域或临时注入(inject)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57419810/

相关文章:

c# - 如何从使用 LINQ to SQL 的 c# 方法返回匿名类型

c# - 在维护 session 的同时将网络场迁移到 asp.net 的运行时版本 4

.net - 当我发出 “SaveChanges()” 时,Entity Framework 中的默认事务隔离级别是什么?

android-studio - 在 Dagger2 中查找提供者的便捷方式

java - 启用 spring aop 回避依赖注入(inject)

c# - 查看 ItemsControl 内的注入(inject)

c# - 使用模拟 Controller 上下文来测试 Controller

c# - 如何根据ID删除元素?

sql - EF5 使用名称中的点生成 SQL Server CE 约束

c# - CaSTLe Windsor 到 Unity - 你能像在 CW 中一样在 Unity 中自动配置吗?