C# 不同存储库类的事务范围

C# 不同存储库类的事务范围,c#,entity-framework,transactions,transactionscope,C#,Entity Framework,Transactions,Transactionscope,我试图将一个事务包装为发生在不同存储库类中的两个或多个数据库操作。每个存储库类使用一个DbContext实例,使用依赖项注入。我使用的是实体框架核心2.1 public PizzaService(IPizzaRepo pizzaRepo, IPizzaIngredientsRepo ingredientRepo) { _pizzaRepo = pizzaRepo; _ingredientRepo = ingredientRepo; } public async Task Sav

我试图将一个事务包装为发生在不同存储库类中的两个或多个数据库操作。每个存储库类使用一个DbContext实例,使用依赖项注入。我使用的是实体框架核心2.1

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

public async Task SavePizza(PizzaViewModel pizza)
{
    using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
    {
        int pizzaRows = await _pizzaRepo.AddEntityAsync(pizza.Pizza);
        int ingredientRows = await _ingredientRepo.PutIngredientsOnPizza(
            pizza.Pizza.PizzaId,
            pizza.Ingredients.Select(x => x.IngredientId).ToArray());

        scope.Complete();
    }
}
}

显然,如果其中一个操作失败,我想回滚整个操作。 这个事务范围是否足以回滚,或者存储库类是否应该拥有自己的事务


即使上述方法有效,是否有更好的方法来实现事务?

存储库模式对于启用测试非常有用,但如果没有新的存储库,请跨存储库共享上下文

作为一个简单的示例(假设您使用的是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 */)
     // ...
   }
}
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 */)
     // ...
   }
}
然后在存储库中:

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 */)
     // ...
   }
}
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 */)
     // ...
   }
}
公共类PizzaRepository:ipzzarepository
{
私有只读IAmbientDbContextLocator _contextLocator=null;
私人比萨背景比萨背景
{
get{return_contextLocator.get();}
}
公共PizzaRepository(IDbContextScopeLocator-contextLocator)
{
_contextLocator=contextLocator;
}
公共异步任务AddEntityAsync(/*params*/)
{
PizzaContext.Pizzas.Add(/*pizza*/)
// ...
}
}
这给了您一些好处:

  • 工作范围单元的控制仍在服务中。您可以调用任意数量的存储库,更改将根据服务的确定进行提交或回滚。(检查结果、捕捉异常等)
  • 该模型在有界上下文中工作得非常好。在较大的系统中,您可以在多个DBContext中分割不同的关注点。上下文定位器作为存储库的一个依赖项,可以访问任何/所有DBContext。(想想日志记录、审计等)
  • 对于在工厂中使用
    CreateReadOnly()
    范围创建的基于读取的操作,还有一个轻微的性能/安全选项。这将创建一个无法保存的上下文范围,因此它保证不会向数据库提交任何写入操作
  • IDbContextScopeFactory和IDbContextScope易于模拟,因此您的服务单元测试可以验证事务是否已提交。(模拟IDbContextScope以断言
    SaveChanges
    ,模拟IDbContextScopeFactory以期望
    Create
    并返回DbContextScope模拟。)在这和存储库模式之间,没有混乱的模拟DBContext

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

    假设您的数据库提供商支持这一点,那么是的。但是,如果您的两个存储库使用两个不同的DbContext实例,这将需要一个分布式事务,这是一种反模式,或者至少不建议用于高频事务。另一方面,如果您注入一个支持这两个存储库的DbContext实例,那么这是绝对正确的。即使Viewmodel属性和entity属性是1:1,是否也不应该将实体传递给客户端?即使如此,我仍然会避免这样做,因为序列化实体可能会触发延迟加载调用或导致发送不完整的实体图。它设定了从客户机返回的实体是完整的期望,并且诱惑是简单地将其附加到上下文&保存更改,而不验证来自客户机的数据没有被篡改。绝对不要相信来自客户的任何东西。(浏览器或API使用者)Automapper可以非常轻松地管理这些映射。