Transaction Scope for different repository classes












1















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?










share|improve this question

























  • 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
















1















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?










share|improve this question

























  • 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














1












1








1








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?










share|improve this question
















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






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Nov 15 '18 at 22:12









abatishchev

70.2k70266397




70.2k70266397










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



















  • 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












1 Answer
1






active

oldest

votes


















0














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:




  1. 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.)

  2. 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.)

  3. 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.

  4. 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 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.






share|improve this answer
























  • 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











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
});


}
});














draft saved

draft discarded


















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









0














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:




  1. 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.)

  2. 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.)

  3. 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.

  4. 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 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.






share|improve this answer
























  • 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
















0














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:




  1. 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.)

  2. 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.)

  3. 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.

  4. 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 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.






share|improve this answer
























  • 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














0












0








0







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:




  1. 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.)

  2. 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.)

  3. 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.

  4. 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 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.






share|improve this answer













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:




  1. 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.)

  2. 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.)

  3. 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.

  4. 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 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.







share|improve this answer












share|improve this answer



share|improve this answer










answered Nov 15 '18 at 22:10









Steve PySteve Py

5,86511019




5,86511019













  • 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











  • 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




















draft saved

draft discarded




















































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.




draft saved


draft discarded














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





















































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







Popular posts from this blog

Florida Star v. B. J. F.

Danny Elfman

Lugert, Oklahoma