存储库模式非常适合启用测试,但没有新的存储库 DbContext,跨存储库共享上下文。
作为一个简单的示例(假设您使用的是 DI/IoC)
DbContext 在您的 IoC 容器中注册,其生命周期范围为 Per Request。所以在服务调用开始时:
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(s) 的生命周期范围,其中存储库不再注入 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 */)
// ...
}
}
这给你带来了几个好处:
- 工作单元范围的控制在服务中保持清晰。您可以调用任意数量的存储库,并且将根据服务的确定提交或回滚更改。 (检查结果、捕捉异常等)
- 此模型非常适用于有界上下文。在较大的系统中,您可能会在多个 DbContext 中拆分不同的关注点。上下文定位器用作存储库的一个依赖项,并且可以访问任何/所有 DbContext。 (想想日志记录、审计等)
- 对于使用工厂中创建的
CreateReadOnly() 范围的基于读取的操作,还有一个轻微的性能/安全选项。这会创建一个无法保存的上下文范围,因此它保证不会向数据库提交任何写入操作。
- IDbContextScopeFactory 和 IDbContextScope 很容易模拟,因此您的服务单元测试可以验证事务是否已提交。 (模拟一个 IDbContextScope 以断言
SaveChanges,模拟一个 IDbContextScopeFactory 以期望一个 Create 并返回 DbContextScope 模拟。)在这和存储库模式之间,没有混乱的模拟 DbContexts。
我在您的示例中看到的一个警告是,您的视图模型似乎正在充当您的实体的包装器。 (PizzaViewModel.Pizza) 我建议不要将实体传递给客户端,而是让视图模型仅代表视图所需的数据。我概述了这个here的原因。