Showing newest 1 of 2 posts from October 2008. Show older posts
Showing newest 1 of 2 posts from October 2008. Show older posts

Thursday, October 9, 2008

The Unit of Work Pattern – Part Deux

Last week’s post, Implementing a persistence ignorant Unit of Work framework (if you haven’t read this post yet I would encourage you to do so as this post builds on the previous post), showed how to implement a foundation for the Unit of Work pattern that can be used to implement specific Unit of Work components for any type of persistence framework.

In this post I’m going to enhance on the existing foundation to add another level to the Unit of Work, something I call the Unit of Work Scope.

Revisiting the current implementation

When working with database intensive applications there are bound to be many database actions that form a single logical operation. Taking the simplest example, a Customer Ordering system, saving an Order itself requires saving the order entity along with all the order detail entities containing the items, quantities, costs, etc.

Using the traditional Unit of Work component, such as one defined in the last post, you would start a Unit of Work, make changes to entities or register entities that need to be added / updated / removed, push those database operations to the server and the finally signal that the unit of work has been completed. The saving of an order in a order processing service could look something like this:

   12             UnitOfWork.Start();

   13             IRepository<Order> ordersRepository = IoC.Container.Resolve<IRepository<Order>>();

   14             ordersRepository.Save(order);

   15             UnitOfWork.Finish(true);

That is simple enough and works. Lets expand on the example and assume that as the order is being saved, we need to reserve all the items in the order in our order reservation system so that the stock represents only quantities that are available. So to provide such logic lets assume a service component called IOrderReservationService has been created. The code can then be refactored to something that resembles this:

   12             UnitOfWork.Start();

   13 

   14             IRepository<Order> ordersRepository = IoC.Container.Resolve<IRepository<Order>>();

   15             ordersRepository.Save(order);

   16 

   17             IOrderReservationService reservationService = IoC.Container.Resolve<IOrderReservationService>();

   18             reservationService.ReserveItemsInOrder(order);

   19 

   20             UnitOfWork.Finish(true);

Now since the implementation of the IOrderReservationService is something we don’t really care about, as long as the component does what is is supposed to do, the order processing service shouldn’t have to to care about the reservation service anyways. But what if the reservation service has the following code in ReserveItemsInOrder method:

   23         public void ReserveItemsInOrder (Order order)

   24         {

   25             IList<ItemReservationEntry> reservationEntries = BuildReservationEntries(order);

   26             UnitOfWork.Start();

   27 

   28             IRepository<ItemReservationEntry> reservationEntryRepository = IoC.Container.Resolve<IRepository<ItemReservationEntry>>();

   29             foreach (var entry in reservationEntries)

   30                 reservationEntryRepository.Save(entry);

   31 

   32             UnitOfWork.Finish(true);

   33         }

The above is a perfectly valid implementation… but has one major flaw. Since the ReserveItemsInOrder also operates under a Unit of Work, logically this method should use the same unit of work as the one started in the order processing service. Furthermore, when the ReserveItemsInOrder method calls UnitOfWork.Finish, the Unit of Work shouldn’t submit changes to the database at that time and mark the unit of work as finished, that is because the changes should be accepted only when the order processing service indicates that all operations have been completed, flush changes and set the Unit of Work as complete.

While in last weeks implementation of the UnitOfWork component satisfies the first requirement, i.e. the ReserveItemsInOrder method should operate under the same unit of work as the unit of work started in the order processing service. This is achieved because the implementation stores the current unit of work in the current thread / request and when UnitOfWork.Start() is called, at that time if an existing unit of work exists, it returns that instance instead of creating a completely new one.

The problem comes in the second requirement. When UnitOfWork.Finish(true) is called within the ReserveItemsInOrder method, that signals the UnitOfWork instance to submit changes to the server, causing both the Order that was added to the orders repository in the order processing service to be inserted and also the ItemReservationEntries to be inserted into the database. This then causes an error when UnitOfWork.Finish(true) is called on the order processing service, because the unit of work has already been finished.

This also causes a logical issue as, the order processing service could take additional steps after reserving item quantities for the order, such as creating an order fulfillment entry so that the order can be processed and dispatched, which should logically fall under the same unit of work, but since the Unit of Work was already finished, it is not possible to do so.

Introducing the Unit of Work scope

To solve the above scenario, we would need to cascade the same Unit of Work down to all components that explicitly start a new unit of work, and add logic to not commit the changes until all logical operations have been completed and the original initiator (i.e. top level component) that started the Unit of Work sets the unit of work as completed.

I’m going to term this as a Unit of Work scope. Some points on the Unit of Work scope:

  • All components operating under a Unit of Work scope will share the same IUnitOfWork instance, unless asked otherwise.
  • A unit of work scope does not commit / flush changes to the database unless it is the top-level unit of work scope.

Whenever I start out to develop a new idea or component, I normally write down some sample syntax of how consumers would consume that API or component. In this case I would like to use (or abuse if you like) the IDisposable pattern to define a scope that ensures that all operations within a using statement runs under the unit of work scope and once the using goes out of scope, either all changes are committed to the database or rolledback. Below is the syntax I would like consumers to use:

   12             using (UnitOfWorkScope scope = new UnitOfWorkScope())

   13             {

   14                 IRepository <Order> ordersRepository = IoC.Container.Resolve<IRepository<Order>>();

   15                 ordersRepository.Save(orders);

   16                 scope.Commit();

   17             }

The above code snippet follows very closely to the way a TransactionScope is used and that is very intentional. When using a TransactionScope, in the System.Transactions namespace, code executing within the scope participates in that TransactionScope. The UnitOfWorkScope uses the same semantics where code within a UnitOfWorkScope automatically participates within that unit of work scope.

Now that we have a syntax that represents the way I would like consumers to start and use the UnitOfWorkScope, lets deal with the big issue with child UnitOfWorkScope instances. Based on the example in the Revisiting the current Implementation section, an issue seems to crop up when a component starts a unit of work and then calls another component that itself starts a unit of work, we need to somehow make that second component utilize the same unit of work instance and not commit any changes until the root component decides to commit.

To tackle this scenario we need a way to somehow pass around the IUnitOfWork instance that will be used by all components. Making things more complex, sometimes there needs to be a way to use a completely different unit of work an not enlist in an existing running unit of work. This scenario happens when you have optional individual operations that if failed do not necessary fail the entire business operation.

The implementation

Based on the above assumptions and requirements, lets start with the implementation. Lets start with a component called the UnitOfWorkScopeTransaction. The intention of this class is to provide a component that encapsulates a IUnitOfWork instance that can be shared by multiple UnitOfWorkScope instances. The reason this class has the name Transaction appended to it is because the UnitOfWorkScopeTransaction class will be responsible for controlling when underlying transaction and deciding on when to actually Commit the unit of work.

Below is the complete code for the UnitOfWorkTransactionScope class:

   17 using System;

   18 using System.Collections.Generic;

   19 using System.Data;

   20 using System.Linq;

   21 

   22 namespace Rhinestone.Shared

   23 {

   24     /// <summary>

   25     /// The <see cref="UnitOfWorkScopeTransaction"/> identifies a unique transaciton that can

   26     /// be shared by multiple <see cref="UnitOfWorkScope"/> instances.

   27     /// </summary>

   28     public class UnitOfWorkScopeTransaction : IDisposable

   29     {

   30         #region fields

   31         private readonly Guid _transactionID;

   32         private readonly IUnitOfWork _unitOfWork;

   33         private readonly ITransaction _runningTransaction;

   34         private readonly Stack<UnitOfWorkScope> _attachedScopes;

   35         private readonly IsolationLevel _isolationLevel;

   36         private bool _transactionRolledback;

   37         private bool _disposed;

   38         #endregion

   39 

   40         #region ctor

   41         /// <summary>

   42         /// Overloaded Constructor.

   43         /// Creates a new instance of the <see cref="UnitOfWorkScopeTransaction"/> that takes in a

   44         /// <see cref="IUnitOfWorkFactory"/> instance that is responsible for creating instances of <see cref="IUnitOfWork"/> and

   45         /// a <see cref="IDbConnection"/> that is used by the instance to connect to the data store.

   46         /// </summary>

   47         /// <param name="unitOfWorkFactory">The <see cref="IUnitOfWorkFactory"/> implementation that is responsible

   48         /// for creating instances of <see cref="IUnitOfWork"/> instances.</param>

   49         /// <param name="isolationLevel">One of the values of <see cref="IsolationLevel"/> that specifies the transaction

   50         /// isolation level of the <see cref="UnitOfWorkScopeTransaction"/> instance.</param>

   51         public UnitOfWorkScopeTransaction(IUnitOfWorkFactory unitOfWorkFactory, IsolationLevel isolationLevel)

   52         {

   53             Guard.Against<ArgumentNullException>(unitOfWorkFactory == null,

   54                                                 "A valid non-null instance that implements the IUnitOfWorkFactory is required.");

   55             _transactionID = new Guid();

   56             _transactionRolledback = false;

   57             _disposed = false;

   58             _unitOfWork = unitOfWorkFactory.Create();

   59             _runningTransaction = _unitOfWork.BeginTransaction(isolationLevel);

   60             _isolationLevel = isolationLevel;

   61             _attachedScopes = new Stack<UnitOfWorkScope>();

   62         }

   63         #endregion

   64 

   65         #region properties

   66         /// <summary>

   67         /// Gets a <see cref="Guid"/> that uniqely identifies the transaction.

   68         /// </summary>

   69         /// <value>A <see cref="Guid"/> that uniquely identifies the transaction.</value>

   70         public Guid TransactionID

   71         {

   72             get { return _transactionID; }

   73         }

   74 

   75         /// <summary>

   76         /// Gets the <see cref="IsolationLevel"/> of the <see cref="UnitOfWorkScopeTransaction"/> instance.

   77         /// </summary>

   78         /// <value>One of the values of <see cref="IsolationLevel"/>.</value>

   79         public IsolationLevel IsolationLevel

   80         {

   81             get { return _isolationLevel; }

   82         }

   83 

   84         /// <summary>

   85         /// Gets the <see cref="IUnitOfWork"/> instance of the <see cref="UnitOfWorkScopeTransaction"/> instance.

   86         /// </summary>

   87         public IUnitOfWork UnitOfWork

   88         {

   89             get { return _unitOfWork; }

   90         }

   91 

   92         /// <summary>

   93         /// Gets a <see cref="IList{T}"/> containing instances of <see cref="UnitOfWorkScopeTransaction"/> currently

   94         /// started for the current request / thread.

   95         /// </summary>

   96         private static IList<UnitOfWorkScopeTransaction> CurrentTransactions

   97         {

   98             get

   99             {

  100                 string key = typeof (UnitOfWorkScopeTransaction).FullName;

  101                 if (!Storage.Local.Contains(key))

  102                     Storage.Local.Set<IList<UnitOfWorkScopeTransaction>>(key, new List<UnitOfWorkScopeTransaction>());

  103                 return Storage.Local.Get<IList<UnitOfWorkScopeTransaction>>(key);

  104             }

  105         }

  106         #endregion

  107 

  108         #region methods

  109         /// <summary>

  110         /// Gets a <see cref="UnitOfWorkScopeTransaction"/> instance that can be used by a <see cref="UnitOfWorkScope"/> instance.

  111         /// </summary>

  112         /// <param name="scope">The <see cref="UnitOfWorkScope"/> instance that is requesting the transaction.</param>

  113         /// <param name="isolationLevel">One of the values of <see cref="IsolationLevel"/> that specifies the transaction isolation level.</param>

  114         /// <returns>A <see cref="UnitOfWorkScopeTransaction"/> instance.</returns>

  115         public static UnitOfWorkScopeTransaction GetTransactionForScope (UnitOfWorkScope scope, IsolationLevel isolationLevel)

  116         {

  117             return GetTransactionForScope(scope, isolationLevel, UnitOfWorkScopeTransactionOptions.UseCompatible);

  118         }

  119 

  120         /// <summary>

  121         /// Gets a <see cref="UnitOfWorkScopeTransaction"/> instance that can be used by a <see cref="UnitOfWorkScope"/> instance.

  122         /// </summary>

  123         /// <param name="scope">The <see cref="UnitOfWorkScope"/> instance that is requesting the transaction.</param>

  124         /// <param name="isolationLevel">One of the values of <see cref="IsolationLevel"/> that specifies the transaction isolation level.</param>

  125         /// <param name="options">One of the values of <see cref="UnitOfWorkScopeTransactionOptions"/> that specifies options for using existing

  126         /// transacitons or creating new ones.</param>

  127         /// <returns>A <see cref="UnitOfWorkScopeTransaction"/> instance.</returns>

  128         public static UnitOfWorkScopeTransaction GetTransactionForScope (UnitOfWorkScope scope, IsolationLevel isolationLevel, UnitOfWorkScopeTransactionOptions options)

  129         {

  130             if (options == UnitOfWorkScopeTransactionOptions.UseCompatible)

  131             {

  132                 var transaction = (from t in CurrentTransactions

  133                                    where t.IsolationLevel == isolationLevel

  134                                    select t).FirstOrDefault();

  135                 if (transaction != null)

  136                 {

  137                     transaction.AttachScope(scope);

  138                     return transaction;

  139                 }

  140             }

  141 

  142             var factory = IoC.Container.Resolve<IUnitOfWorkFactory>();

  143             var newTransaction = new UnitOfWorkScopeTransaction(factory, isolationLevel);

  144             newTransaction.AttachScope(scope);

  145             CurrentTransactions.Add(newTransaction);

  146             return newTransaction;

  147         }

  148 

  149         /// <summary>

  150         /// Attaches a <see cref="UnitOfWorkScope"/> instance to the <see cref="UnitOfWorkScopeTransaction"/> instance.

  151         /// </summary>

  152         /// <param name="scope"></param>

  153         private void AttachScope(UnitOfWorkScope scope)

  154         {

  155             Guard.Against<ObjectDisposedException>(_disposed, "Transaction has been disposed. Cannot attach a scope to a disposed transaction.");

  156             Guard.Against<ArgumentNullException>(scope == null, "Cannot attach a null UnitOfWorkScope instance to the UnitOfWorkScopeTransaction instance.");

  157             _attachedScopes.Push(scope); //Push the scope on to the top of the stack.

  158         }

  159 

  160         /// <summary>

  161         /// Causes a comit operation on the <see cref="UnitOfWorkScopeTransaction"/> instance.

  162         /// </summary>

  163         /// <param name="scope">The <see cref="UnitOfWorkScope"/> instance that is calling the commit.</param>

  164         /// <remarks>

  165         /// This method can only by called by the scope currently on top of the stack. If Called by another scope then an

  166         /// <see cref="InvalidOperationException"/> is called. If the calling scope is last in the attached scope hierarchy,

  167         /// then a commit is called on the underling unit of work instance.

  168         /// </remarks>

  169         public void Commit(UnitOfWorkScope scope)

  170         {

  171             Guard.Against<ObjectDisposedException>(_disposed, "Transaction has been disposed. Cannot commit a disposed transaction.");

  172             Guard.Against<InvalidOperationException>(_transactionRolledback,

  173                                                     "Cannot call commit on a rolledback transaction. A child scope or current scope has already rolled back the transaction. Call Rollback()");

  174             Guard.Against<ArgumentNullException>(scope == null,

  175                                                 "Cannot commit the transaction for a null UnitOfWorkScope instance.");

  176             Guard.Against<InvalidOperationException>(_attachedScopes.Peek() != scope,

  177                                                     "Commit can only be called by the current UnitOfWorkScope instance. The UnitOfWorkScope provided does not match the current scope on the stack."); //TODO: Fix wording of exception.

  178             _attachedScopes.Pop();

  179             if (_attachedScopes.Count == 0)

  180             {

  181                 //The calling UnitOfWorkScope is the root of the transaction.

  182                 _unitOfWork.Flush();

  183                 _runningTransaction.Commit();

  184                 _runningTransaction.Dispose();

  185                 _unitOfWork.Dispose();

  186                 CurrentTransactions.Remove(this);

  187             }

  188         }

  189 

  190         /// <summary>

  191         /// Causes a Rollback operation on the <see cref="UnitOfWorkScopeTransaction"/> instance.

  192         /// </summary>

  193         /// <param name="scope">The <see cref="UnitOfWorkScope"/> instance that is calling the commit.</param>

  194         /// <remarks>

  195         /// This method can only be called by the scope currently on top of the stack. If called by another scope than the

  196         /// current <see cref="UnitOfWorkScope"/> instance, then a <see cref="InvalidOperationException"/> is thrown. If the

  197         /// calling scope is the last in the attached scope hierarchy, then a rollback is called on the underlying UnitOfWork

  198         /// instance.

  199         /// </remarks>

  200         public void Rollback(UnitOfWorkScope scope)

  201         {

  202             Guard.Against<ObjectDisposedException>(_disposed, "Transaction has been disposed. Cannot rollback a disposed transaction.");

  203             Guard.Against<ArgumentNullException>(scope == null, "Cannot rollback the transaction for a null UnitOfWork instance.");

  204             Guard.Against<InvalidOperationException>(_attachedScopes.Peek() != scope, "Rollback can only be called by the current UnitOfWorkScope instance. The UnitOfWorkScope provided does not match the current scope on the stack."); //TODO: Fix wording of exception.

  205 

  206             _attachedScopes.Pop();

  207             _transactionRolledback = true;

  208             if (_attachedScopes.Count == 0)

  209             {

  210                 //The calling UnitOfWorkScope is the root of the transaction.

  211                 _runningTransaction.Rollback();

  212                 _runningTransaction.Dispose();

  213                 _unitOfWork.Dispose();

  214                 CurrentTransactions.Remove(this);

  215             }

  216         }

  217         #endregion

  218 

  219         #region Implementation of IDisposable

  220         /// <summary>

  221         /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.

  222         /// </summary>

  223         /// <filterpriority>2</filterpriority>

  224         public void Dispose()

  225         {

  226             if (!_disposed)

  227             {

  228                 _disposed = true;

  229                 GC.SuppressFinalize(this);

  230             }

  231         }

  232         #endregion

  233     }

  234 }

A UnitOfWorkTransactionScope instance maintains a Stack of all scopes that share this transaction which allows it to do is maintain a hierarchal list of all scopes that are currently attached to it. When a scope that is sharing this UnitOfWorkTransactionScope calls Commit / Rollback, the UnitOfWorkTransactionScope instance performs the following checks and actions:

  1. A check is made to ensure that the calling UnitOfWorkScope should be on top of a local field called _attachedScopes, a Stack<UnitOfWorkScope>, indicating that this is the top most scope on the current thread. [I’ll get to how UnitOfWorkScope instances are added to _attachedScopes in a bit]

    This is done to ensure that out of turn Commit or Rollback calls are not made. Consider the following scenario;

       12         public void ScopeAMethod()

       13         {

       14             using (UnitOfWorkScope scopeA = new UnitOfWorkScope())

       15             {

       16                 ScopeBMethod();

       17                 scopeA.Commit();

       18             }

       19         }

       20 

       21         private void ScopeBMethod()

       22         {

       23             using (UnitOfWorkScope scopeB = new UnitOfWorkScope())

       24             {

       25                 scopeB.Commit();

       26             }

       27         }


    What the above dipicts a very common scenario where a root method starts a UnitOfWorkScope and then calls another method that stars another child UnitOfWorkScope where the child Scope should either be committed or rolledback first before the parent scope can be committed or rolledback. If the parent scope calls Commit before the child scope has had a change to vote on the commit or rolledback, that is an out of turn commit or rollback. This check guards the UnitOfWorkScopeTransaction from this scenario.

  2. If the call is Commit, then the instance also checks to ensure that a child scope did not not vote for rollback. If so, Commit cannot be honored and a InvalidOperationException is thrown.
  3. After some additional checks for null references and disposed check, the top most scope in the _attachedScopes Stack is popped. Then the instance checks if there are any more scopes in the stack, if not this is the last and final call to Commit / Rollback, and performs the appropriate action on the underlying IUnitOfWork / ITransaction component.

So how do UnitOfWorkScope instances get on the _attachedScopes stack of a UnitOfWorkScopeTransaction instance? That is via the RegisterScope instance method on the UnitOfWorkScopeTransaction class. Below is a sequence diagram of how a UnitOfWorkScope instance attaches itself to a UnitOfWorkScopeTransaction instance class:

 image 

Now that we have a way to share and pass around the underlying IUnitOfWork instance between UnitOfWorkScope instances, below is the complete implementation of the UnitOfWorkScope class:

   17 using System;

   18 using System.Collections.Generic;

   19 using System.Data;

   20 

   21 namespace Rhinestone.Shared

   22 {

   23     /// <summary>

   24     /// Helper class that allows starting and using a unit of work like:

   25     /// <![CDATA[

   26     ///    using (UnitOfWorkScope scope = new UnitOfWorkScope()) {

   27     ///        //Do some stuff here.

   28     ///        scope.Commit();

   29     ///    }

   30     ///

   31     /// ]]>

   32     /// </summary>

   33     public class UnitOfWorkScope : IDisposable

   34     {

   35         #region fields

   36         private static readonly string UnitOfWorkScopeStackKey = typeof (UnitOfWorkScope).FullName + ".RunningScopeStack";

   37         private UnitOfWorkScopeTransaction _currentTransaction;

   38         private bool _disposed;

   39         #endregion

   40 

   41         #region ctor

   42         /// <summary>

   43         /// Default Constuctor.

   44         /// Creates a new <see cref="UnitOfWorkScope"/> with the <see cref="IsolationLevel.Serializable"/>

   45         /// transaction isolation level.

   46         /// </summary>

   47         public UnitOfWorkScope() : this(IsolationLevel.ReadCommitted, UnitOfWorkScopeTransactionOptions.UseCompatible) {}

   48 

   49         /// <summary>

   50         /// Overloaded Constructor.

   51         /// Creates a new instance of <see cref="UnitOfWorkScope"/> with the specified transaction

   52         /// isolation level.

   53         /// </summary>

   54         /// <param name="isolationLevel">One of the values of <see cref="IsolationLevel"/> that specifies

   55         /// the transation isolation level the scope should use.</param>

   56         public UnitOfWorkScope (IsolationLevel isolationLevel) : this(isolationLevel, UnitOfWorkScopeTransactionOptions.UseCompatible) {}

   57 

   58         /// <summary>

   59         /// Overloaded Constructor.

   60         /// Creates a new instance of <see cref="UnitOfWorkScope"/> with the specified transaction isolation level, option connection and

   61         /// a transaction option that specifies if an existing transaction should be used or to create a new transaction.

   62         /// </summary>

   63         /// <param name="isolationLevel"></param>

   64         /// <param name="transactionOptions"></param>

   65         public UnitOfWorkScope (IsolationLevel isolationLevel, UnitOfWorkScopeTransactionOptions transactionOptions)

   66         {

   67             _disposed = false;

   68             _currentTransaction = UnitOfWorkScopeTransaction.GetTransactionForScope(this, isolationLevel, transactionOptions);

   69             RegisterScope(this);

   70         }

   71         #endregion

   72 

   73         #region properties

   74         /// <summary>

   75         /// Checks if the current thread or request has a <see cref="UnitOfWorkScope"/> instance started.

   76         /// </summary>

   77         /// <value>True if a <see cref="UnitOfWorkScope"/> instance has started and is present.</value>

   78         public static bool HasStarted

   79         {

   80             get

   81             {

   82                 if (!Storage.Local.Contains(UnitOfWorkScopeStackKey))

   83                     return false;

   84                 return RunningScopes.Count > 0;

   85             }

   86         }

   87 

   88         /// <summary>

   89         /// Gets the current <see cref="UnitOfWorkScope"/> instance for the current thread or request.

   90         /// </summary>

   91         /// <value>The current and most recent <see cref="UnitOfWorkScope"/> instance started for the current thread or request.

   92         /// If none started, then a null reference is returned.</value>

   93         public static UnitOfWorkScope Current

   94         {

   95             get

   96             {

   97                 if (RunningScopes.Count == 0)

   98                     return null;

   99                 return RunningScopes.Peek();

  100             }

  101         }

  102 

  103         /// <summary>

  104         /// Gets a <see cref="Stack{T}"/> of <see cref="UnitOfWorkScope"/> that is used to store and retrieve

  105         /// running scope instances.

  106         /// </summary>

  107         private static Stack<UnitOfWorkScope> RunningScopes

  108         {

  109             get

  110             {

  111                 //Note: No locking is required since the stack is stored either on the current thread or on the current request.

  112                 if (!Storage.Local.Contains(UnitOfWorkScopeStackKey))

  113                     Storage.Local.Set(UnitOfWorkScopeStackKey, new Stack<UnitOfWorkScope>());

  114                 return Storage.Local.Get <Stack<UnitOfWorkScope>>(UnitOfWorkScopeStackKey);

  115             }

  116         }

  117 

  118         /// <summary>

  119         /// Gets the <see cref="IUnitOfWork"/> instance used by the <see cref="UnitOfWorkScope"/> instance.

  120         /// </summary>

  121         public IUnitOfWork UnitOfWork

  122         {

  123             get

  124             {

  125                 if (!HasStarted)

  126                     return null;

  127                 return _currentTransaction.UnitOfWork;

  128             }

  129         }

  130         #endregion

  131 

  132         #region methods

  133         /// <summary>

  134         /// Registers a scope as the top level scope on the <see cref="RunningScopes"/> stack.

  135         /// </summary>

  136         /// <param name="scope">The <see cref="UnitOfWorkScope"/> instance to set as the top level scope on the stack.</param>

  137         private static void RegisterScope(UnitOfWorkScope scope)

  138         {

  139             Guard.Against<ArgumentNullException>(scope == null, "Cannot register a null UnitOfWorkScope instance as the top level scope.");

  140             Shared.UnitOfWork.Current = scope.UnitOfWork; //Setting the UnitOfWork isntance held by the scope as the current scope.

  141             RunningScopes.Push(scope);

  142         }

  143 

  144         /// <summary>

  145         /// UnRegisters a <see cref="UnitOfWorkScope"/> as the top level scope on the stack.

  146         /// </summary>

  147         /// <param name="scope"></param>

  148         private static void UnRegisterScope (UnitOfWorkScope scope)

  149         {

  150             Guard.Against<ArgumentNullException>(scope == null, "Cannot Un-Register a null UnitOfWorkScope instance as the top level scope.");

  151             Guard.Against<InvalidOperationException>(RunningScopes.Peek() != scope, "The UnitOfWorkScope provided does not match the current top level scope. Cannot un-register the specified scope.");

  152             RunningScopes.Pop();

  153 

  154             if (RunningScopes.Count > 0)

  155             {

  156                 //If the Stack has additional scopes, set the current unit of work to the UnitOfWork instance held by the top most scope.

  157                 UnitOfWorkScope currentScope = RunningScopes.Peek();

  158                 Shared.UnitOfWork.Current = currentScope.UnitOfWork;

  159             }

  160             else

  161                 Shared.UnitOfWork.Current = null;

  162         }

  163 

  164         ///<summary>

  165         /// Commits the current running transaction in the scope.

  166         ///</summary>

  167         public void Commit()

  168         {

  169             Guard.Against<ObjectDisposedException>(_disposed, "Cannot commit a disposed UnitOfWorkScope instance.");

  170             _currentTransaction.Commit(this);

  171             _currentTransaction = null;

  172         }

  173 

  174         /// <summary>

  175         /// Disposes off the <see cref="UnitOfWorkScope"/> insance.

  176         /// </summary>

  177         public void Dispose()

  178         {

  179             if (!_disposed)

  180             {

  181                 if (_currentTransaction != null)

  182                 {

  183                     _currentTransaction.Rollback(this);

  184                     _currentTransaction = null;

  185                 }

  186                 UnRegisterScope(this);

  187 

  188                 GC.SuppressFinalize(this);

  189                 _disposed = true;

  190             }

  191         }

  192         #endregion

  193     }

  194 }

The UnitOfWorkScope class is fairly simple, when an instance is created it calls the GetTransactionForScope static method on the UnitOfWorkScopeTransaction class which returns a UnitOfWorkScopeTransaction instance for the scope. When Commit is called on the UnitOfWorkScope instance, it calls Commit on the underlying UnitOfWorkScopeTransaction instance, or if the object is disposed before a commit, then a Rollback is called on the UnitOfWorkScopeTransaction.

The only complex part of this implementation is how UnitOfWorkScope supports multiple scopes that could run within a hierarchy that do not share the same transaction, and how to expose the current running UnitOfWork to the Repositories.

Lets assume the following example:

   12         public void ScopeAMethod()

   13         {

   14             using (UnitOfWorkScope scopeA = new UnitOfWorkScope())

   15             {

   16                 ScopeBMethod();

   17                 scopeA.Commit();

   18             }

   19         }

   20 

   21         private void ScopeBMethod()

   22         {

   23             using (UnitOfWorkScope scopeB = new UnitOfWorkScope(IsolationLevel.ReadCommitted, UnitOfWorkScopeTransactionOptions.CreateNew))

   24             {

   25                 ScopeCMethod();

   26                 scopeB.Commit();

   27             }

   28         }

   29 

   30         private void ScopeCMethod()

   31         {

   32             using (UnitOfWorkScope scopeC = new UnitOfWorkScope())

   33             {

   34                 scopeC.Commit();

   35             }

   36         }

In the above example, ScopeB requests a UnitOfWorkScope that does not share an existing UnitOfWorkScopeTransaction  instance and forces the instance to use its own instance. So what we have here is ScopeA using a IUnitOfWork, lets say uowA, and ScopeB and ScopeC share one IUnitOfWork, uowB.

Lets also assume that repositories will use the UnitOfWork.Current property to get the current IUnitOfWork instance. So what we have is a scenario where when executing under ScopeA, a repository should get uowA as the unit of work to operate on, when moving to ScopeB, the repository should get uowB, same in the case of ScopeC. Now when ScopeB is disposed, a repository should get back to using uowA.

This follows the semantics of a Stack frame. As you create UnitOfWorkScope instances, they are put onto a Stack that push the IUnitOfWork that is tied with to that scope. As the scope goes out of… ah scope… the IUnitOfWork tied to that scope should be popped along with the scope and the next scope on the stack should then push it’s IUnitOfWork back as the current unit of work to use.

And my head hurts, yours might be hurting as well.

Anyways, this Stack frame like semantics is tackled by using the ReigsterScope and UnRegisterScope methods. As instances of UnitOfWorkScopes are created they call RegisterScope forcing the IUnitOfWork tied to that scope as the current IUnitOfWork. As a UnitOfWorkScope is dispoed it calls the UnRegisterScope method that removes the scope from the stack and if a scope on the stack still exists, then the method sets the IUnitOfWork associated with the scope, now on top of the stack, as the current IUnitOfWork.

Final thoughts

The UnitOfWorkScope might seem complicated enough at first, but it does provide elegance and semantics to consumer code which is well worth the implementation. If follows existing design patterns closely, like TransactionScope, and is transparent enough so that you can extend it to add your own logic if required. Speaking of TransactionScope, if the underlying IDbConnection supports transaction enlistment, the UnitOfWorkScope implementation is perfectly compatible with TransactionScope.

Full source code, along with tests, can be found in Rhinestone’s source tree. Now that some of the foundational and infrastructure elements are complete, future posts are going to focus back on Rhinestone and it’s implementation.

Rhinestone Source: http://rhinestone.googlecode.com/svn

I would appreciate any feedback on bugs or inconsistencies in logic or even general comments. Thanks!

Submit this story to DotNetKicks