Transaction Scope for different repository classes
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?
c# entity-framework transactions transactionscope
add a comment |
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?
c# entity-framework transactions transactionscope
Assuming your database provider supports this, then yes. But if your two repositories use two different DbContext instances, this will require a distributed transaction, which is an anti-pattern, or at least not recommended for high-frequency transactions. On the other hand if you inject a single DbContext instance that supports both the repositories, then this is absolutely fine.
– David Browne - Microsoft
Nov 15 '18 at 17:15
add a comment |
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?
c# entity-framework transactions transactionscope
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?
c# entity-framework transactions transactionscope
c# entity-framework transactions transactionscope
edited Nov 15 '18 at 22:12
abatishchev
70.1k70265397
70.1k70265397
asked Nov 15 '18 at 17:07
Michiel WoutersMichiel Wouters
3916
3916
Assuming your database provider supports this, then yes. But if your two repositories use two different DbContext instances, this will require a distributed transaction, which is an anti-pattern, or at least not recommended for high-frequency transactions. On the other hand if you inject a single DbContext instance that supports both the repositories, then this is absolutely fine.
– David Browne - Microsoft
Nov 15 '18 at 17:15
add a comment |
Assuming your database provider supports this, then yes. But if your two repositories use two different DbContext instances, this will require a distributed transaction, which is an anti-pattern, or at least not recommended for high-frequency transactions. On the other hand if you inject a single DbContext instance that supports both the repositories, then this is absolutely fine.
– David Browne - Microsoft
Nov 15 '18 at 17:15
Assuming your database provider supports this, then yes. But if your two repositories use two different DbContext instances, this will require a distributed transaction, which is an anti-pattern, or at least not recommended for high-frequency transactions. On the other hand if you inject a single DbContext instance that supports both the repositories, then this is absolutely fine.
– David Browne - Microsoft
Nov 15 '18 at 17:15
Assuming your database provider supports this, then yes. But if your two repositories use two different DbContext instances, this will require a distributed transaction, which is an anti-pattern, or at least not recommended for high-frequency transactions. On the other hand if you inject a single DbContext instance that supports both the repositories, then this is absolutely fine.
– David Browne - Microsoft
Nov 15 '18 at 17:15
add a comment |
1 Answer
1
active
oldest
votes
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:
- The control of the unit of work scope remains clearly in the service. You can call any number of repositories and the changes will be committed, or rolled back based on the determination of the service. (inspecting results, catching exceptions, etc.)
- This model works extremely well with bounded contexts. In larger systems you may split different concerns across multiple DbContexts. The context locator serves as one dependency for a repository and can access any/all DbContexts. (Think logging, auditing, etc.)
- There is also a slight performance/safety option for Read-based operations using the
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. - The IDbContextScopeFactory and IDbContextScope are easily mock-able so that your service unit tests can validate if a transaction is committed or not. (Mock an IDbContextScope to assert
SaveChanges
, and mock an IDbContextScopeFactory to expect aCreate
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.
Thank you for the extensive answer. Should an entity never be passed to the client, even if the Viewmodel properties and Entity properties are 1:1 alike?
– Michiel Wouters
Nov 16 '18 at 14:53
I would still avoid it even then because serializing an entity can trigger lazy load calls or result in an incomplete entity graph being sent. It sets the expectation that the entity coming back from a client is complete, and the temptation is there to simply attach it to a context & save changes without verifying the data from the client hasn't been tampered with. Trust absolutely nothing coming from a client. (browser or API consumer) Automapper can manage these mappings quite easily.
– Steve Py
Nov 17 '18 at 5:31
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53324601%2ftransaction-scope-for-different-repository-classes%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
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:
- The control of the unit of work scope remains clearly in the service. You can call any number of repositories and the changes will be committed, or rolled back based on the determination of the service. (inspecting results, catching exceptions, etc.)
- This model works extremely well with bounded contexts. In larger systems you may split different concerns across multiple DbContexts. The context locator serves as one dependency for a repository and can access any/all DbContexts. (Think logging, auditing, etc.)
- There is also a slight performance/safety option for Read-based operations using the
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. - The IDbContextScopeFactory and IDbContextScope are easily mock-able so that your service unit tests can validate if a transaction is committed or not. (Mock an IDbContextScope to assert
SaveChanges
, and mock an IDbContextScopeFactory to expect aCreate
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.
Thank you for the extensive answer. Should an entity never be passed to the client, even if the Viewmodel properties and Entity properties are 1:1 alike?
– Michiel Wouters
Nov 16 '18 at 14:53
I would still avoid it even then because serializing an entity can trigger lazy load calls or result in an incomplete entity graph being sent. It sets the expectation that the entity coming back from a client is complete, and the temptation is there to simply attach it to a context & save changes without verifying the data from the client hasn't been tampered with. Trust absolutely nothing coming from a client. (browser or API consumer) Automapper can manage these mappings quite easily.
– Steve Py
Nov 17 '18 at 5:31
add a comment |
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:
- The control of the unit of work scope remains clearly in the service. You can call any number of repositories and the changes will be committed, or rolled back based on the determination of the service. (inspecting results, catching exceptions, etc.)
- This model works extremely well with bounded contexts. In larger systems you may split different concerns across multiple DbContexts. The context locator serves as one dependency for a repository and can access any/all DbContexts. (Think logging, auditing, etc.)
- There is also a slight performance/safety option for Read-based operations using the
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. - The IDbContextScopeFactory and IDbContextScope are easily mock-able so that your service unit tests can validate if a transaction is committed or not. (Mock an IDbContextScope to assert
SaveChanges
, and mock an IDbContextScopeFactory to expect aCreate
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.
Thank you for the extensive answer. Should an entity never be passed to the client, even if the Viewmodel properties and Entity properties are 1:1 alike?
– Michiel Wouters
Nov 16 '18 at 14:53
I would still avoid it even then because serializing an entity can trigger lazy load calls or result in an incomplete entity graph being sent. It sets the expectation that the entity coming back from a client is complete, and the temptation is there to simply attach it to a context & save changes without verifying the data from the client hasn't been tampered with. Trust absolutely nothing coming from a client. (browser or API consumer) Automapper can manage these mappings quite easily.
– Steve Py
Nov 17 '18 at 5:31
add a comment |
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:
- The control of the unit of work scope remains clearly in the service. You can call any number of repositories and the changes will be committed, or rolled back based on the determination of the service. (inspecting results, catching exceptions, etc.)
- This model works extremely well with bounded contexts. In larger systems you may split different concerns across multiple DbContexts. The context locator serves as one dependency for a repository and can access any/all DbContexts. (Think logging, auditing, etc.)
- There is also a slight performance/safety option for Read-based operations using the
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. - The IDbContextScopeFactory and IDbContextScope are easily mock-able so that your service unit tests can validate if a transaction is committed or not. (Mock an IDbContextScope to assert
SaveChanges
, and mock an IDbContextScopeFactory to expect aCreate
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.
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:
- The control of the unit of work scope remains clearly in the service. You can call any number of repositories and the changes will be committed, or rolled back based on the determination of the service. (inspecting results, catching exceptions, etc.)
- This model works extremely well with bounded contexts. In larger systems you may split different concerns across multiple DbContexts. The context locator serves as one dependency for a repository and can access any/all DbContexts. (Think logging, auditing, etc.)
- There is also a slight performance/safety option for Read-based operations using the
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. - The IDbContextScopeFactory and IDbContextScope are easily mock-able so that your service unit tests can validate if a transaction is committed or not. (Mock an IDbContextScope to assert
SaveChanges
, and mock an IDbContextScopeFactory to expect aCreate
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.
answered Nov 15 '18 at 22:10
Steve PySteve Py
5,80011018
5,80011018
Thank you for the extensive answer. Should an entity never be passed to the client, even if the Viewmodel properties and Entity properties are 1:1 alike?
– Michiel Wouters
Nov 16 '18 at 14:53
I would still avoid it even then because serializing an entity can trigger lazy load calls or result in an incomplete entity graph being sent. It sets the expectation that the entity coming back from a client is complete, and the temptation is there to simply attach it to a context & save changes without verifying the data from the client hasn't been tampered with. Trust absolutely nothing coming from a client. (browser or API consumer) Automapper can manage these mappings quite easily.
– Steve Py
Nov 17 '18 at 5:31
add a comment |
Thank you for the extensive answer. Should an entity never be passed to the client, even if the Viewmodel properties and Entity properties are 1:1 alike?
– Michiel Wouters
Nov 16 '18 at 14:53
I would still avoid it even then because serializing an entity can trigger lazy load calls or result in an incomplete entity graph being sent. It sets the expectation that the entity coming back from a client is complete, and the temptation is there to simply attach it to a context & save changes without verifying the data from the client hasn't been tampered with. Trust absolutely nothing coming from a client. (browser or API consumer) Automapper can manage these mappings quite easily.
– Steve Py
Nov 17 '18 at 5:31
Thank you for the extensive answer. Should an entity never be passed to the client, even if the Viewmodel properties and Entity properties are 1:1 alike?
– Michiel Wouters
Nov 16 '18 at 14:53
Thank you for the extensive answer. Should an entity never be passed to the client, even if the Viewmodel properties and Entity properties are 1:1 alike?
– Michiel Wouters
Nov 16 '18 at 14:53
I would still avoid it even then because serializing an entity can trigger lazy load calls or result in an incomplete entity graph being sent. It sets the expectation that the entity coming back from a client is complete, and the temptation is there to simply attach it to a context & save changes without verifying the data from the client hasn't been tampered with. Trust absolutely nothing coming from a client. (browser or API consumer) Automapper can manage these mappings quite easily.
– Steve Py
Nov 17 '18 at 5:31
I would still avoid it even then because serializing an entity can trigger lazy load calls or result in an incomplete entity graph being sent. It sets the expectation that the entity coming back from a client is complete, and the temptation is there to simply attach it to a context & save changes without verifying the data from the client hasn't been tampered with. Trust absolutely nothing coming from a client. (browser or API consumer) Automapper can manage these mappings quite easily.
– Steve Py
Nov 17 '18 at 5:31
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53324601%2ftransaction-scope-for-different-repository-classes%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Assuming your database provider supports this, then yes. But if your two repositories use two different DbContext instances, this will require a distributed transaction, which is an anti-pattern, or at least not recommended for high-frequency transactions. On the other hand if you inject a single DbContext instance that supports both the repositories, then this is absolutely fine.
– David Browne - Microsoft
Nov 15 '18 at 17:15