I'm trying to wrap a transaction around 2 or more database operations which occur in different repository classes. Each repository class uses a DbContext instance, using Dependency Injection. I'm using Entity Framework Core 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();
}
}
}
Obviously, if one of the operations fails, I want to rollback the entire thing. Will this transaction scope be enough to rollback or should the repository classes have transactions on their own?
Even if above methods works, are there better ways to implement transactions?
Repository patterns are great for enabling testing, but do not have a repository new up a DbContext, share the context across repositories.
As a bare-bones example (assuming you are using DI/IoC)
The DbContext is registered with your IoC container with a lifetime scope of Per Request. So at the onset of the service call:
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();
}
Then in the repositories:
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 */)
// ...
}
}
The trouble I have with this pattern is that it restricts the unit of work to the request, and only the request. You have to be aware of when and where the context save changes occurs. You don't want repositories for example to call SaveChanges as that could have side effects depending on what was changed as far as the context goes prior to that being called.
As a result I use a Unit of Work pattern to manage the lifetime scope of the DbContext(s) where repositories no longer get injected with a DbContext, they instead get a locator, and the services get a context scope factory. (Unit of work) The implementation I use for EF(6) is Mehdime's DbContextScope. (https://github.com/mehdime/DbContextScope) There are forks available for EFCore. (https://www.nuget.org/packages/DbContextScope.EfCore/) With the DBContextScope the service call looks more like:
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();
}
}
Then in the repositories:
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 */)
// ...
}
}
This gives you a couple benefits:
CreateReadOnly() scope creation in the factory. This creates a context scope that cannot be saved so it guarantees no write operations get committed to the database.SaveChanges, and mock an IDbContextScopeFactory to expect a Create and return the DbContextScope mock.) Between that and the Repository pattern, No messy mocking DbContexts.One caution that I see in your example is that it appears that your View Model is serving as a wrapper for your entity. (PizzaViewModel.Pizza) I'd advise against ever passing an entity to the client, rather let the view model represent just the data that is needed for the view. I outline the reasons for this here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With