不同存储库类别的交易范围

时间:2018-11-15 17:07:24

标签: c# entity-framework transactions transactionscope

我正在尝试将事务存储在不同存储库类中发生的2个或更多数据库操作周围。每个存储库类都使用DbContext实例(通过依赖注入)。我正在使用Entity Framework Core 2.1。

POST https://graph.microsoft.com/v1.0/subscriptions
Content-Type: application/json
{
  "changeType": "created,updated",
  "notificationUrl": "https://webhook.azurewebsites.net/notificationClient",
  "resource": "/me/mailfolders('inbox')/messages",
  "expirationDateTime": "2016-03-20T11:00:00.0000000Z",
  "clientState": "SecretClientState"
}

}

很显然,如果其中一项操作失败,我想回滚整个过程。 此事务作用域是否足以回滚,或者存储库类是否应具有自己的事务?

即使以上方法可行,还有更好的方法来实现交易吗?

1 个答案:

答案 0 :(得分:0)

存储库模式非常适合启用测试,但是没有新的DbContext存储库,而是在存储库之间共享上下文。

作为一个简单的例子(假设您正在使用DI / IoC)

DbContext已在您的IoC容器中注册,其生存期范围为“每个请求”。因此,在服务呼叫开始时:

public PizzaService(PizzaDbContext context, IPizzaRepo pizzaRepo, IPizzaIngredientsRepo ingredientRepo)
{
  _context = pizzaContext;
  _pizzaRepo = pizzaRepo;
  _ingredientRepo = ingredientRepo;
}

public async Task SavePizza(PizzaViewModel pizza)
{
  int pizzaRows = await _pizzaRepo.AddEntityAsync(pizza.Pizza);
  int ingredientRows = await _ingredientRepo.PutIngredientsOnPizza(
    pizza.Pizza.PizzaId,
    pizza.Ingredients.Select(x => x.IngredientId).ToArray());

  _context.SaveChanges();
} 

然后在存储库中:

public class PizzaRepository : IPizzaRepository
{
  private readonly PizzaDbContext _pizzaDbContext = null;

  public PizzaRepository(PizzaDbContext pizzaDbContext)
  {
    _pizzaDbContext = pizzaDbContext;
  }

  public async Task<int> AddEntityAsync( /* params */ )
  {
     PizzaContext.Pizzas.Add( /* pizza */)
     // ...
   }
}

这种模式给我带来的麻烦是它将工作单元限制为仅请求。您必须知道上下文保存更改的时间和位置。例如,您不希望存储库调用SaveChanges,因为它可能会产生副作用,具体取决于在调用上下文之前进行的更改。

因此,我使用工作单元模式来管理DbContext的生存期范围,在该范围中,不再向存储库注入DbContext,而是获取定位器,而服务获得上下文范围工厂。 (工作单元)我用于EF(6)的实现是Mehdime的DbContextScope。 (https://github.com/mehdime/DbContextScope)EFCore有可用的分叉。 (https://www.nuget.org/packages/DbContextScope.EfCore/)使用DBContextScope,服务调用看起来更像:

public PizzaService(IDbContextScopeFactory contextScopeFactory, IPizzaRepo pizzaRepo, IPizzaIngredientsRepo ingredientRepo)
{
  _contextScopeFactory = contextScopeFactory;
  _pizzaRepo = pizzaRepo;
  _ingredientRepo = ingredientRepo;
}

public async Task SavePizza(PizzaViewModel pizza)
{
  using (var contextScope = _contextScopeFactory.Create())
  {
    int pizzaRows = await _pizzaRepo.AddEntityAsync(pizza.Pizza);
    int ingredientRows = await _ingredientRepo.PutIngredientsOnPizza(
      pizza.Pizza.PizzaId,
      pizza.Ingredients.Select(x => x.IngredientId).ToArray());

    contextScope.SaveChanges();
  }
}  

然后在存储库中:

public class PizzaRepository : IPizzaRepository
{
  private readonly IAmbientDbContextLocator _contextLocator = null;

  private PizzaContext PizzaContext
  {
    get { return _contextLocator.Get<PizzaContext>(); }
  }

  public PizzaRepository(IDbContextScopeLocator contextLocator)
  {
    _contextLocator = contextLocator;
  }

  public async Task<int> AddEntityAsync( /* params */ )
  {
     PizzaContext.Pizzas.Add( /* pizza */)
     // ...
   }
}

这给您带来了几个好处:

  1. 对工作单元范围的控制仍然清楚地存在于服务中。您可以调用任意数量的存储库,并且根据服务的确定,更改将被提交或回滚。 (检查结果,捕获异常等)
  2. 此模型在有限的上下文中非常有效。在较大的系统中,您可以在多个DbContext中分配不同的关注点。上下文定位器充当存储库的一个依赖项,并且可以访问任何/所有DbContext。 (请考虑日志记录,审核等)
  3. 在工厂中使用CreateReadOnly()范围创建,对于基于读取的操作也有一些性能/安全性选项。这样会创建一个无法保存的上下文范围,因此可以确保不会将任何写操作提交给数据库。
  4. IDbContextScopeFactory和IDbContextScope易于模拟,因此您的服务单元测试可以验证是否提交事务。 (模拟一个IDbContextScope断言SaveChanges,并模拟一个IDbContextScopeFactory期望一个Create并返回DbContextScope模拟。)在那和存储库模式之间,不要凌乱地模拟DbContext。

在您的示例中看到的一个警告是,您的视图模型似乎充当了实体的包装器。 (PizzaViewModel.Pizza)我建议不要将实体传递给客户端,而应让视图模型仅代表视图所需的数据。我概述了此here的原因。