Using TransactionScope to Keep Repository Concerns Separate

Sunday, November 08 2009

Alright, so you’re using the Repository Pattern for your persistence needs. Within the repositories you’re using the Adapter Pattern provided with LLBLGen in order to access your persistence layer. Everything is going great. But then you encounter an issue: Multiple queries need to be executed, and all must be executed within a single transaction. How could you accomplish this? Well, the adapter pattern provides a simple implementation.

using (var myAdapter = new DataAccessAdapter())
{
    myAdapter.StartTransaction(IsolationLevel.ReadCommitted, "Transaction Name");
    try
    {
        // Execute queries using myAdapter here.
        myAdapter.Commit();
    }
    catch (Exception)
    {
        myAdapter.Rollback();
    }
}

All you have to do is call StartTransaction on the adapter, giving it an isolation level and a name, then call either Commit or Rollback on the adapter when you’re done. This works, but begins interfering with the Single Responsibility Principle. Say you have to insert a new Pizza into your database, but also have to insert any new Toppings that were not in your database before. On top of that, you need to have a Price record for this Pizza. Now you're making multiple updates to the database in a single repository method. Which repository would you put this under? You would probably concede putting it in your PizzaRepository, as that’s the focal point of this operation. However, you already have a nice Add method in your IngredientRepository, and you wouldn’t want to repeat yourself in PizzaRepository’s Add method, now would you?

An quick solution to this problem would be to just pass around your active adapter from repository to repository. This would, however, expose infrastructure details to your interface details, which reside in the Core of your application (if you’re using the ONION architecture, that is). It is usually unwise to be passing around active database connections in the first place as well.

The solution to preserve our Separation of Concerns and our Single Responsibility Principle is to utilize the TransactionScope class, which is located in System.Transactions. LLBLGen’s adapters work directly with this class, and will change their end-result behavior depending on whether or not the TransactionScope is committed. Notice that this is built right into the .NET Framework, so you could use this in your Core project if necessary. Hooray! Now, instead of depending on keeping a single adapter alive to execute a transaction, you can merely wrap your repository calls in a single TransactionScope.

Pizza pizza = new Pizza();
pizza.Ingredients = new List<Ingredient>();
// Populate pizza and pizza.Ingredients here.
 
// Newing up repositories here only for the sake of brevity.
IPizzaRepository pizzaRepository = new PizzaRepository();
IIngredientRepository ingredientRepository = new IngredientRepository();
 
using (var transactionScope = new TransactionScope())
{
    bool isPizzaSaved = pizzaRepository.Save(pizza);
    bool areIngredientsSaved = ingredientRepository.Save(pizza.Ingredients);
    if (isPizzaSaved && areIngredientsSaved)
    {
        transactionScope.Complete();
    }
}

 

In order to commit a TransactionScope’s transaction, you simply call Complete on the TransactionScope object. If Complete is not called, the transaction will automatically roll back. In fact, there is no rollback method, so you have to rely on this behavior. You could also wrap your repository calls in a try/catch block if they throw exceptions when something goes wrong as well.

With TransactionScope, you could create a Controller class or something similar which can house all of the repositories you depend on to complete the transaction. Furthermore, all of your repositories can be transaction-ignorant: They may be part of a transaction, or they may not. The point is that they don’t have to care. They just execute their single responsibility and are done!

To get TransactionScopes to work is slightly less than intuitive, however. In my next post I will detail how to setup both the client machine and the database-serving machine so that you can use TransactionScopes.