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:
- 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.
- 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.
- 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:
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!