Meeting the needs of your business from a distance

Unit of Work Pattern for Entity Framework 4 and Unity

by Mark Shiffer 28. June 2010 13:43

Continuing from my last post on the Repository pattern…here is some background.

I am in the process of bringing up a new architecture for a project that I am working on that uses MVVM with WPF, WCF, Unity and Entity Framework 4 with Self Tracking Entities. One of the foundational items that is necessary to create as part of this architecture is a Repository to abstract the data store. As a related item, a proper Unit of Work pattern needs to be instantiated in order to provide transactional boundaries for some of the data processes. I will focus on the Unit of Work in this post, for more information on my repository implementation please see my previous post.

Before I started designing my approach I read several different attempts by others at implementing these patterns. Some were better than others, but in the end no one approach seemed to be a best fit for my application. However, the heaviest influence on my design came from NCommon, a library that contains implementations of commonly used design patterns when developing applications. If you haven’t looked at NCommon before, I would suggest taking a gander as it has some interesting stuff. However, for my purposes, it introduced too much complexity by trying to be a generic framework that applied to several different technologies, whereas, directing my approach to my specific set of technologies (while still abstracting mind you), made the code much more straight forward.

With Entity Framework it naturally works out that the ObjectContext is the unit of work itself. The ObjectContext is already caching all of the data changes that occur during its lifetime and only commits those when asked. So, at the core of my Unit of Work implementation is the ObjectContext. I have defined an IUnitOfWork interface which the ObjectContext implements to further abstract the concept. After that there are 3 core classes involved in my implementation of Unit of Work:

1. UnitOfWorkScope – The purpose of this class is to provide a quick fluid interface to place a unit of work around a block of code. This is the core class that is used by the ‘outside’ world to create transactional code. It’s intended usage pattern is:

   1: using (var scope = new UnitOfWorkScope())
   2: {
   3:     ...
   4:     scope.Commit();
   5: }

2. TransactionManager – The TransactionManager is in charge of placing a UnitOfWorkScope into a UnitOfWorkTransaction whether it be new or existing.

3. UnitOfWorkTransaction – This is the actual transaction that controls when, and if, work completed within a set of UnitOfWorkScopes will be committed to the data store. When a UnitOfWorkTransaction is instantiated it creates a new IUnitOfWork instance (ObjectContext) on which to act.

So the developer creates scopes, nesting additional scopes where needed or creating side-by-side scopes of a separate transactional boundary. When a scope is created, it asks the TransactionManager to be enlisted in a transaction. The TransactionManager finds or creates an UnitOfWorkTransaction and associates it with the UnitOfWorkScope. The UnitOfWorkTransaction then keeps a list of active scopes for the transaction. When any of the scopes is rolled back, the entire transaction is torn down. Once the last scope is committed, assuming no rollbacks have occurred, the UnitOfWorkTransaction flushes the IUnitOfWork effectively calling SaveChanges on the ObjectContext.

If you’ve read the Repository post prior to this, you may have noticed where unit of work was tied in making the process very seamless for the end developer. The CurrentObjectSet property of the repository class coordinates with the TransactionManager such that regardless of where the repository instance was created, it will join a transaction on the fly if one is active. In addition, ApplyChanges will only save to the data store if the repository instance is not under a transaction at that time.

So, let’s start with the interfaces for what set’s all of this up:

IUnitOfWork:

   1: /// <summary>
   2:  /// Unit of work interface. The EF ObjectContext serves as the unit of work. This interface provides a contract to interact with the EF ObjectContext
   3:  /// as a unit of work.s
   4:  /// </summary>
   5:  public interface IUnitOfWork : IDisposable
   6:  {
   7:      /// <summary>
   8:      /// Gets the context for this unit of work (EF Context serves as the actual unit of work).
   9:      /// </summary>
  10:      /// <value>The context.</value>
  11:      ObjectContext Context { get; }
  12:  
  13:      /// <summary>
  14:      /// Commits changes to the Context to the data store.
  15:      /// </summary>
  16:      void Flush();
  17:  
  18:      /// <summary>
  19:      /// Rollbacks changes to the Context.
  20:      /// </summary>
  21:      void Rollback();
  22:  }

IUnitOfWorkScope:

   1: /// <summary>
   2: /// Unit of work scope is the outward facing wrapper for developers to begin/enlist in a transaction. Scopes can be nested, in which case, once the last or outtermost
   3: /// scope is committed then the unit of work will be flushed/committed. If any scope exists/disposes before committing it will be rolled back, and any
   4: /// scopes upward in the chain will be rolled back as well. 
   5: /// </summary>
   6: public interface IUnitOfWorkScope : IDisposable
   7: {
   8:     /// <summary>
   9:     /// Marks the scope committed, triggering the unit of work to commit if this is the outtermost scope.
  10:     /// </summary>
  11:     void Commit();
  12:  
  13:     /// <summary>
  14:     /// Rollbacks this scope, triggering the unit of work to be rolled back.
  15:     /// </summary>
  16:     void Rollback();
  17: }

ITransactionManager:

   1: /// <summary>
   2: /// The Transaction Manager holds a list of active, isolated transactions within the system and acts as the entry point for a new unit of work scope. 
   3: /// </summary>
   4: public interface ITransactionManager
   5: {
   6:     /// <summary>
   7:     /// Gets the current EF context based upon the most recent transacion.
   8:     /// </summary>
   9:     /// <value>The current context.</value>
  10:     ObjectContext CurrentContext { get; }
  11:  
  12:     /// <summary>
  13:     /// Gets or sets the thread static operation context, the unique identifier for a wcf call across our system.
  14:     /// </summary>
  15:     /// <value>The operation context.</value>
  16:     /// <param name="operationContext">The operation context.</param>
  17:     Guid OperationContext { get; set; }
  18:  
  19:     /// <summary>
  20:     /// Enlists a <see cref="UnitOfWorkScope"/> instance with the transaction manager, either beginning a new transaction or attaching to an existing transaction.
  21:     /// </summary>
  22:     /// <param name="scope">bool. True if the scope should be enlisted in a new transaction, else
  23:     /// false if the scope should participate in the existing transaction when available</param>
  24:     /// <param name="newTransaction">if set to <c>true</c> [new transaction].</param>
  25:     /// <returns>
  26:     /// Transcation created or assigned to the scope.
  27:     /// </returns>
  28:     IUnitOfWorkTransaction EnlistScope(IUnitOfWorkScope scope, bool newTransaction);
  29: }

IUnitOfWorkTransaction:

   1: /// <summary>
   2: /// Unit Of Work Transaction keeps a list of active scopes for a given transaction and commits the unit of work if all scopes have been committed.
   3: /// </summary>
   4: public interface IUnitOfWorkTransaction : IDisposable
   5: {
   6:     /// <summary>
   7:     /// Occurs when [transaction disposing].
   8:     /// </summary>
   9:     event EventHandler<EventArgs> TransactionDisposing;
  10:  
  11:     /// <summary>
  12:     /// Gets the unit of work.
  13:     /// </summary>
  14:     /// <value>The unit of work.</value>
  15:     IUnitOfWork UnitOfWork { get; }
  16:  
  17:     /// <summary>
  18:     /// Gets a unique identifier for a series of related operations. Under WCF this is provided by its operation context which is unique for each call made to the service.
  19:     /// </summary>
  20:     /// <value>The operation context identifier.</value>
  21:     Guid OperationContext { get; }
  22:  
  23:     /// <summary>
  24:     /// Enlists the scope in the transaction
  25:     /// </summary>
  26:     /// <param name="scope">The scope.</param>
  27:     void EnlistScope(IUnitOfWorkScope scope);
  28:  
  29:     /// <summary>
  30:     /// Commits the scope, if this is the last scope that was enlisted, the unit of work will be flushed.
  31:     /// </summary>
  32:     /// <param name="scope">The scope.</param>
  33:     void CommitScope(IUnitOfWorkScope scope);
  34:  
  35:     /// <summary>
  36:     /// Rollbacks the scope, effectively rolling back the unit of work.
  37:     /// </summary>
  38:     /// <param name="scope">The scope.</param>
  39:     void RollbackScope(IUnitOfWorkScope scope);
  40: }

All pretty straight forward. The only special note here is the OperationContext. This is what helps to make the pattern work across threads. This is especially important with WCF involved, as each call to the server will be on its own thread. In addition, server job processes could spawn additional threads to perform their work and one would expect work completed on a child thread should join the transaction of its parent. I’ll talk more about this as we look at the implementations of the interfaces.

<UnitOfWork>:

   1: /// <summary> Provide business logic for the MSSWCModelContainer class 
   2: /// </summary>
   3: [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Need interface with IDisposable so that code can call base disposable generically.")]
   4: public partial class MSSWCModelContainer : IUnitOfWork
   5: {
   6:     /// <summary>
   7:     /// Gets the context.
   8:     /// </summary>
   9:     /// <value>The context.</value>
  10:     public ObjectContext Context
  11:     {
  12:         get { return this; }
  13:     }
  14:  
  15:     /// <summary> Persists all updates to the data source with the specified <see cref="T:System.Data.Objects.SaveOptions"/>.
  16:     /// </summary>
  17:     /// <param name="options">A <see cref="T:System.Data.Objects.SaveOptions"/> value that determines the behavior of the operation.</param>
  18:     /// <returns>
  19:     /// The number of objects in an <see cref="F:System.Data.EntityState.Added"/>, <see cref="F:System.Data.EntityState.Modified"/>, or <see cref="F:System.Data.EntityState.Deleted"/> state when <see cref="M:System.Data.Objects.ObjectContext.SaveChanges"/> was called.
  20:     /// </returns>
  21:     /// <exception cref="T:System.Data.OptimisticConcurrencyException">An optimistic concurrency violation has occurred.</exception>
  22:     public override int SaveChanges(SaveOptions options)
  23:     {
  24:         // Validate the state of each entity in the context
  25:         // before SaveChanges can succeed.
  26:         foreach (ObjectStateEntry entry in ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Modified))
  27:         {
  28:             if (!entry.IsRelationship)
  29:             {
  30:                 if (entry.Entity is IValidatingEntity)
  31:                 {
  32:                     ((IValidatingEntity)entry.Entity).Validate();
  33:                 }
  34:  
  35:                 if (entry.Entity is ISrsEntity)
  36:                 {
  37:                     ApplyLastModifiedInformation((ISrsEntity)entry.Entity);
  38:                 }
  39:             }
  40:         }
  41:  
  42:         List<IObjectWithChangeTracker> entitiesToReset = null;
  43:  
  44:         if ((options & SaveOptions.AcceptAllChangesAfterSave) == SaveOptions.AcceptAllChangesAfterSave)
  45:         {
  46:             var q = from entry in ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified)
  47:                     where entry.Entity is IObjectWithChangeTracker
  48:                     select (IObjectWithChangeTracker)entry.Entity;
  49:  
  50:             entitiesToReset = q.ToList();
  51:         }
  52:  
  53:         var result = base.SaveChanges(options);
  54:  
  55:         if (entitiesToReset != null)
  56:         {
  57:             foreach (var entity in entitiesToReset)
  58:             {
  59:                 entity.AcceptChanges();
  60:             }
  61:         }
  62:  
  63:         return result;
  64:     }
  65:  
  66:     /// <summary>
  67:     /// Commits this instance.
  68:     /// </summary>
  69:     public void Flush()
  70:     {
  71:         this.SaveChanges();
  72:     }
  73:  
  74:     /// <summary>
  75:     /// Rollbacks this instance.
  76:     /// </summary>
  77:     public void Rollback()
  78:     {
  79:         // rolling back invalidates this object context for any future use, so dispose it.
  80:         Dispose();
  81:     }
  82:  
  83:     /// <summary>
  84:     /// Writes the last modified date/time and user information to the entity.
  85:     /// </summary>
  86:     /// <param name="entity">Entity to apply last modified info to.</param>
  87:     private static void ApplyLastModifiedInformation(ISrsEntity entity)
  88:     {
  89:         entity.LastModified = DateTime.Now;
  90:         if (ServiceSecurityContext.Current != null)
  91:         {
  92:             entity.ModifiedBy = ServiceSecurityContext.Current.WindowsIdentity.Name;
  93:         }
  94:     }
  95: }

Not much to see here, moving on… Ignore the nastiness of the SaveChanges call and the AcceptChanges loop. Once again, Entity Framework has some issues.

UnitOfWorkScope:

   1: /// <summary>
   2: /// Unit of work scope is the outward facing wrapper for developers to begin/enlist in a transaction. Scopes can be nested, in which case, once the last or outtermost
   3: /// scope is committed then the unit of work will be flushed/committed. If any scope exists/disposes before committing it will be rolled back, and any
   4: /// scopes upward in the chain will be rolled back as well.
   5: /// </summary>
   6: public class UnitOfWorkScope : IUnitOfWorkScope, IDisposable
   7: {
   8:     private bool disposed;
   9:     private bool commitAttempted;
  10:     private IUnitOfWorkTransaction transaction;
  11:     private ITransactionManager transactionManager;
  12:  
  13:     /// <summary>
  14:     /// Initializes a new instance of the <see cref="UnitOfWorkScope"/> class with the <see cref="System.Data.IsolationLevel.Serializable"/>
  15:     /// transaction isolation level.
  16:     /// </summary>
  17:     public UnitOfWorkScope()
  18:         : this(false)
  19:     {
  20:     }
  21:  
  22:     /// <summary>
  23:     /// Initializes a new instance of the <see cref="UnitOfWorkScope"/> class.
  24:     /// </summary>
  25:     /// <param name="newTransaction">To create a new scope that does not enlist in an existing ambient
  26:     /// <see cref="UnitOfWorkScope"/> or <see cref="TransactionScope"/>, specify new, otherwise specify false.</param>
  27:     public UnitOfWorkScope(bool newTransaction)
  28:     {
  29:         transaction = TransactionManager.EnlistScope(this, newTransaction);
  30:         transaction.TransactionDisposing += new EventHandler<EventArgs>(OnTransactionDisposing);
  31:         ScopeStatus = UnitOfWorkScopeStatus.Active;
  32:     }
  33:  
  34:     /// <summary>
  35:     /// Initializes a new instance of the <see cref="UnitOfWorkScope"/> class and enlists in a specific existing transaction.
  36:     /// </summary>
  37:     /// <param name="transaction">The transaction to enlist the new scope in.</param>
  38:     public UnitOfWorkScope(IUnitOfWorkTransaction transaction)
  39:     {
  40:         if (transaction != null)
  41:         {
  42:             this.transaction = transaction;
  43:             transaction.EnlistScope(this);
  44:             ScopeStatus = UnitOfWorkScopeStatus.Active;
  45:             transaction.TransactionDisposing += new EventHandler<EventArgs>(OnTransactionDisposing);
  46:         }
  47:         else
  48:         {
  49:             // if the transaction wasn't specified then call the transaction manager to enlist the scope as it will find/create a transaction for the scope to attach to.
  50:             transaction = TransactionManager.EnlistScope(this, false);
  51:             ScopeStatus = UnitOfWorkScopeStatus.Active;
  52:             transaction.TransactionDisposing += new EventHandler<EventArgs>(OnTransactionDisposing);
  53:         }
  54:     }
  55:  
  56:     /// <summary>
  57:     /// Event fired when the scope is comitting.
  58:     /// </summary>
  59:     public event EventHandler<EventArgs> ScopeComitting;
  60:  
  61:     /// <summary>
  62:     /// Event fired when the scope is rollingback.
  63:     /// </summary>
  64:     public event EventHandler<EventArgs> ScopeRollingback;
  65:  
  66:     /// <summary>
  67:     /// Gets the current scope status.
  68:     /// </summary>
  69:     /// <value>The scope status.</value>
  70:     public UnitOfWorkScopeStatus ScopeStatus
  71:     {
  72:         get;
  73:         private set;
  74:     }
  75:  
  76:     /// <summary>
  77:     /// Gets the transaction manager.
  78:     /// </summary>
  79:     /// <value>The transaction manager.</value>
  80:     private ITransactionManager TransactionManager
  81:     {
  82:         get
  83:         {
  84:             if (transactionManager == null)
  85:             {
  86:                 transactionManager = ServiceLocator.Current.GetInstance<ITransactionManager>();
  87:             }
  88:  
  89:             return transactionManager;
  90:         }
  91:     }
  92:  
  93:     /// <summary>
  94:     /// Commits the current running transaction in the scope.
  95:     /// </summary>
  96:     public void Commit()
  97:     {
  98:         if (disposed)
  99:         {
 100:             throw new ObjectDisposedException("Cannot commit a disposed UnitOfWorkScope instance.");
 101:         }
 102:  
 103:         commitAttempted = true;
 104:  
 105:         if (ScopeComitting != null)
 106:         {
 107:             ScopeComitting(this, EventArgs.Empty);
 108:         }
 109:  
 110:         if (transaction != null)
 111:         {
 112:             transaction.CommitScope(this);
 113:         }
 114:  
 115:         ScopeStatus = UnitOfWorkScopeStatus.Complete;
 116:     }
 117:  
 118:     /// <summary>
 119:     /// Rollback the unit of work.
 120:     /// </summary>
 121:     public void Rollback()
 122:     {
 123:         if (ScopeRollingback != null)
 124:         {
 125:             ScopeRollingback(this, EventArgs.Empty);
 126:         }
 127:  
 128:         if (transaction != null)
 129:         {
 130:             transaction.RollbackScope(this);
 131:         }
 132:  
 133:         ScopeStatus = UnitOfWorkScopeStatus.Complete;
 134:     }
 135:  
 136:     /// <summary>
 137:     /// Disposes off the <see cref="UnitOfWorkScope"/> insance.
 138:     /// </summary>
 139:     public void Dispose()
 140:     {
 141:         Dispose(true);
 142:         GC.SuppressFinalize(this);
 143:     }
 144:  
 145:     /// <summary>
 146:     /// Disposes off the managed and un-managed resources used.
 147:     /// </summary>
 148:     /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
 149:     private void Dispose(bool disposing)
 150:     {
 151:         if (disposed)
 152:         {
 153:             return;
 154:         }
 155:  
 156:         if (disposing)
 157:         {
 158:             try
 159:             {
 160:                 if (!commitAttempted)
 161:                 {
 162:                     Rollback();
 163:                 }
 164:             }
 165:             finally
 166:             {
 167:                 ScopeComitting = null;
 168:                 ScopeRollingback = null;
 169:                 disposed = true;
 170:             }
 171:         }
 172:     }
 173:  
 174:     /// <summary>
 175:     /// Handles the TransactionDisposing event of the transaction control.
 176:     /// </summary>
 177:     /// <param name="sender">The source of the event.</param>
 178:     /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
 179:     private void OnTransactionDisposing(object sender, EventArgs e)
 180:     {
 181:         transaction.TransactionDisposing -= new EventHandler<EventArgs>(OnTransactionDisposing);
 182:         transaction = null;
 183:         ScopeStatus = UnitOfWorkScopeStatus.Complete;
 184:     }
 185: }

Again pretty straight forward. The TransactionManager that the scope uses for enlisting is service located, eventually pulled from Unity. This is done to keep the usage scenario fluent, otherwise each time a scope is instantiated a TransactionManager would have to be found to inject into the scope. That would either result in a lot of calls to service location spread around the code, or in the Unity container being passed around. Both of which are undesirable.

Take note, too, that disposing a scope will automatically trigger a rollback unless the scope was explicitly committed prior to the disposal via a call to Commit(). Also note, that additional flexibility is provided via the constructor overloads, allowing a UnitOfWorkScope to explicitly begin a new transaction or to enlist in a specific existing transaction. The default behavior is to enlist in the existing ambient transaction if one exists, otherwise to create one.

TransactionManager:

   1: /// <summary>
   2: /// The Transaction Manager holds a list of active, isolated transactions within the system and acts as the entry point for a new unit of work scope. 
   3: /// </summary>
   4: public class TransactionManager : ITransactionManager, IDisposable
   5: {
   6:     /// <summary>
   7:     /// Dictionary of operation contexts to transactions operating under that context
   8:     /// </summary>
   9:     private readonly Dictionary<Guid, List<IUnitOfWorkTransaction>> transactionLists = new Dictionary<Guid, List<IUnitOfWorkTransaction>>();
  10:  
  11:     [ThreadStatic]
  12:     private static Guid operationContext;
  13:  
  14:     private bool disposed;
  15:     private object transactionsLockObject = new object();
  16:     private IUnitOfWorkTransactionFactory transactionFactory;
  17:  
  18:     /// <summary>
  19:     /// Initializes a new instance of the <see cref="TransactionManager"/> class.
  20:     /// </summary>
  21:     /// <param name="transactionFactory">The transaction factory.</param>
  22:     public TransactionManager(IUnitOfWorkTransactionFactory transactionFactory)
  23:     {
  24:         this.transactionFactory = transactionFactory;
  25:     }
  26:  
  27:     /// <summary>
  28:     /// Gets the current context of the most recent transaction's unit of work.
  29:     /// </summary>
  30:     /// <value>The current context.</value>
  31:     public ObjectContext CurrentContext
  32:     {
  33:         get
  34:         {
  35:             if (CurrentTransaction != null)
  36:             {
  37:                 return CurrentTransaction.UnitOfWork.Context;
  38:             }
  39:             else
  40:             {
  41:                 return null;
  42:             }
  43:         }
  44:     }
  45:  
  46:     /// <summary>
  47:     /// Gets or sets the operation context that the current thread is running under.
  48:     /// Transactions are tied to an operation context, so that additional scopes that join a transaction should only join if they belong to the same context.
  49:     /// This value is currently being set in the AuthorizationPolicy so that each call to WCF tags its thread with a unique context. If new threads are spawned
  50:     /// by processes, they must manually copy the operation context from the main thread to their sub-threads.
  51:     /// </summary>
  52:     /// <value>The operation context, unique per wcf call.</value>
  53:     /// <param name="operationContext">The operation context.</param>
  54:     public Guid OperationContext
  55:     {
  56:         get
  57:         {
  58:             return operationContext;
  59:         }
  60:  
  61:         set
  62:         {
  63:             operationContext = value;
  64:         }
  65:     }
  66:  
  67:     /// <summary>
  68:     /// Gets the most recent transaction.
  69:     /// </summary>
  70:     /// <value>The current transaction.</value>
  71:     private IUnitOfWorkTransaction CurrentTransaction
  72:     {
  73:         get
  74:         {
  75:             lock (transactionsLockObject)
  76:             {
  77:                 List<IUnitOfWorkTransaction> transactions;
  78:                 if (transactionLists.TryGetValue(OperationContext, out transactions))
  79:                 {
  80:                     if (transactions.Count > 0)
  81:                     {
  82:                         return transactions[transactions.Count - 1];
  83:                     }
  84:                 }
  85:  
  86:                 return null;
  87:             }
  88:         }
  89:     }
  90:  
  91:     /// <summary>
  92:     /// Enlists a <see cref="UnitOfWorkScope"/> instance with the transaction manager.
  93:     /// </summary>
  94:     /// <param name="scope">bool. True if the scope should be enlisted in a new transaction, else
  95:     /// false if the scope should participate in the existing transaction</param>
  96:     /// <param name="newTransaction">if set to <c>true</c> [new transaction].</param>
  97:     /// <returns>
  98:     /// Transcation created or assigned to the scope.
  99:     /// </returns>
 100:     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed outside of this scope.")]
 101:     public IUnitOfWorkTransaction EnlistScope(IUnitOfWorkScope scope, bool newTransaction)
 102:     {
 103:         if (newTransaction || CurrentTransaction == null)
 104:         {
 105:             IUnitOfWorkTransaction transaction = transactionFactory.CreateUnitOfWorkTransaction(OperationContext);
 106:             transaction.TransactionDisposing += OnTransactionDisposing;
 107:  
 108:             lock (transactionsLockObject)
 109:             {
 110:                 transaction.EnlistScope(scope);
 111:                 List<IUnitOfWorkTransaction> transactions;
 112:                 if (!transactionLists.TryGetValue(OperationContext, out transactions))
 113:                 {
 114:                     transactions = new List<IUnitOfWorkTransaction>();
 115:                     transactionLists.Add(OperationContext, transactions);
 116:                 }
 117:  
 118:                 transactions.Add(transaction);
 119:             }
 120:  
 121:             return transaction;
 122:         }
 123:         else
 124:         {
 125:             CurrentTransaction.EnlistScope(scope);
 126:             return CurrentTransaction;
 127:         }
 128:     }
 129:  
 130:     /// <summary>
 131:     /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 132:     /// </summary>
 133:     /// <filterpriority>2</filterpriority>
 134:     public void Dispose()
 135:     {
 136:         Dispose(true);
 137:         GC.SuppressFinalize(this);
 138:     }
 139:  
 140:     /// <summary>
 141:     /// Releases unmanaged and - optionally - managed resources
 142:     /// </summary>
 143:     /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
 144:     private void Dispose(bool disposing)
 145:     {
 146:         if (disposed)
 147:         {
 148:             return;
 149:         }
 150:  
 151:         if (disposing)
 152:         {
 153:             lock (transactionsLockObject)
 154:             {
 155:                 if (transactionLists != null && transactionLists.Count > 0)
 156:                 {
 157:                     foreach (var kvp in transactionLists)
 158:                     {
 159:                         foreach (var transaction in kvp.Value)
 160:                         {
 161:                             transaction.TransactionDisposing -= OnTransactionDisposing;
 162:                             transaction.Dispose();
 163:                         }
 164:                     }
 165:  
 166:                     transactionLists.Clear();
 167:                 }
 168:             }
 169:         }
 170:  
 171:         disposed = true;
 172:     }
 173:  
 174:     /// <summary>
 175:     /// Called when [transaction disposing].
 176:     /// </summary>
 177:     /// <param name="sender">The sender.</param>
 178:     /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
 179:     private void OnTransactionDisposing(object sender, EventArgs e)
 180:     {
 181:         IUnitOfWorkTransaction transaction = sender as IUnitOfWorkTransaction;
 182:         if (transaction != null)
 183:         {
 184:             transaction.TransactionDisposing -= OnTransactionDisposing;
 185:             lock (transactionsLockObject)
 186:             {
 187:                 List<IUnitOfWorkTransaction> transactions;
 188:                 if (transactionLists.TryGetValue(transaction.OperationContext, out transactions))
 189:                 {
 190:                     if (transactions.Contains(transaction))
 191:                     {
 192:                         transactions.Remove(transaction);
 193:                     }
 194:                 }
 195:             }
 196:         }
 197:     }

It’s getting a little more interesting now. One TransactionManager will exist for the entire WCF service, requiring some coordination of the active transactions, especially when one thinks about the threading that is happening in the service. The private field transactionLists, tracks all open transactions by their OperationContext. The OperationContext is simply a Guid that is set at the very beginning of each Wcf Service call. Take special note that the context is marked as ThreadStatic, so that one instance of operationContext exists for each thread, even though there is only one instance of the TransactionManager for the service as a whole. If a job process instantiates child threads to complete its work, it is the responsibility of the coder at that time to carry the operation context through and to set it on the TransactionManager.

UnitOfWorkTransaction:

   1: /// <summary>
   2: /// Unit Of Work Transaction keeps a list of active scopes for a given transaction and commits the unit of work if all scopes have been committed.
   3: /// </summary>
   4: public class UnitOfWorkTransaction : IUnitOfWorkTransaction, IDisposable
   5: {
   6:     private bool disposed;
   7:     private IUnitOfWork unitOfWork;
   8:     private IList<IUnitOfWorkScope> attachedScopes = new List<IUnitOfWorkScope>();
   9:     private object attachedScopesLock = new object();
  10:  
  11:     /// <summary>
  12:     /// Initializes a new instance of the <see cref="UnitOfWorkTransaction"/> class.
  13:     /// </summary>
  14:     /// <param name="unitOfWorkFactory">The context factory; used to create a new unit of work instance.</param>
  15:     /// <param name="operationContext">The operation context, a unique identifier for each WCF call in our system, stored in TransactionManager's thread static property.</param>
  16:     public UnitOfWorkTransaction(IUnitOfWorkFactory unitOfWorkFactory, Guid operationContext)
  17:     {
  18:         if (unitOfWorkFactory == null)
  19:         {
  20:             throw new ArgumentNullException("unitOfWorkFactory");
  21:         }
  22:  
  23:         this.unitOfWork = unitOfWorkFactory.CreateUnitOfWork();
  24:  
  25:         this.OperationContext = operationContext;
  26:     }
  27:  
  28:     /// <summary>
  29:     /// Raised when the transaction is disposing.
  30:     /// </summary>
  31:     public event EventHandler<EventArgs> TransactionDisposing;
  32:  
  33:     /// <summary>
  34:     /// Gets the <see cref="IUnitOfWork"/> instance managed by the 
  35:     /// <see cref="UnitOfWorkTransaction"/> instance.
  36:     /// </summary>
  37:     public IUnitOfWork UnitOfWork
  38:     {
  39:         get { return unitOfWork; }
  40:     }
  41:  
  42:     /// <summary>
  43:     /// Gets the thread id for the transaction. Transactions are tied to a thread, so that additional scopes that join a transaction should only join if they belong to the same thread.
  44:     /// </summary>
  45:     /// <value>The thread id.</value>
  46:     public Guid OperationContext
  47:     {
  48:         get;
  49:         private set;
  50:     }
  51:  
  52:     /// <summary>
  53:     /// Attaches a <see cref="UnitOfWorkScope"/> instance to the 
  54:     /// <see cref="UnitOfWorkTransaction"/> instance.
  55:     /// </summary>
  56:     /// <param name="scope">The <see cref="UnitOfWorkScope"/> instance to attach.</param>
  57:     public void EnlistScope(IUnitOfWorkScope scope)
  58:     {
  59:         if (scope == null)
  60:         {
  61:             throw new ArgumentNullException("scope");
  62:         }
  63:  
  64:         if (!attachedScopes.Contains(scope))
  65:         {
  66:             lock (attachedScopesLock)
  67:             {
  68:                 if (!attachedScopes.Contains(scope))
  69:                 {
  70:                     attachedScopes.Add(scope);
  71:                 }
  72:             }
  73:         }
  74:     }
  75:  
  76:     /// <summary>
  77:     /// Callback executed when an enlisted scope has comitted.
  78:     /// </summary>
  79:     /// <param name="scope">The scope.</param>
  80:     public void CommitScope(IUnitOfWorkScope scope)
  81:     {
  82:         if (disposed)
  83:         {
  84:             throw new ObjectDisposedException("The transaction attached to the scope has already been disposed.");
  85:         }
  86:  
  87:         lock (attachedScopesLock)
  88:         {
  89:             if (!attachedScopes.Contains(scope))
  90:             {
  91:                 throw new InvalidOperationException("The scope being comitted is not attached to the current transaction.");
  92:             }
  93:  
  94:             attachedScopes.Remove(scope);
  95:             if (attachedScopes.Count == 0)
  96:             {
  97:                 try
  98:                 {
  99:                     unitOfWork.Flush();
 100:                 }
 101:                 finally
 102:                 {
 103:                     // Dispose the transaction after comitting.
 104:                     Dispose();
 105:                 }
 106:             }
 107:         }
 108:     }
 109:  
 110:     /// <summary>
 111:     /// Callback executed when an enlisted scope is rolledback.
 112:     /// </summary>
 113:     /// <param name="scope">The scope.</param>
 114:     public void RollbackScope(IUnitOfWorkScope scope)
 115:     {
 116:         if (disposed)
 117:         {
 118:             throw new ObjectDisposedException("The transaction attached to the scope has already been disposed.");
 119:         }
 120:  
 121:         // dispose this transcation which will notify via TransactionDisposing all scopes that were attached that the 
 122:         // transaction is being rolled back
 123:         Dispose();
 124:     }
 125:  
 126:     /// <summary>
 127:     /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 128:     /// </summary>
 129:     /// <filterpriority>2</filterpriority>
 130:     public void Dispose()
 131:     {
 132:         Dispose(true);
 133:         GC.SuppressFinalize(this);
 134:     }
 135:  
 136:     /// <summary>
 137:     /// Releases unmanaged and - optionally - managed resources
 138:     /// </summary>
 139:     /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
 140:     private void Dispose(bool disposing)
 141:     {
 142:         if (disposed)
 143:         {
 144:             return;
 145:         }
 146:  
 147:         if (disposing)
 148:         {
 149:             // notify all who care that this transaction is done.
 150:             if (TransactionDisposing != null)
 151:             {
 152:                 TransactionDisposing(this, EventArgs.Empty);
 153:             }
 154:  
 155:             // dispose of the unit of work since it was created for this transaction specifically
 156:             if (unitOfWork != null)
 157:             {
 158:                 unitOfWork.Dispose();
 159:             }
 160:  
 161:             lock (attachedScopesLock)
 162:             {
 163:                 attachedScopes.Clear();
 164:                 attachedScopes = null;
 165:             }
 166:  
 167:             TransactionDisposing = null;
 168:             unitOfWork = null;
 169:             disposed = true;
 170:         }
 171:     }
 172: }

Nothing surprising in this implementation given the previous discussion. The UnitOfWorkTransaction then keeps a list of active scopes for the transaction. When any of the scopes is rolled back, the entire transaction is torn down. Once the last scope is committed, assuming no roll backs have occurred, the UnitOfWorkTransaction flushes the IUnitOfWork effectively calling SaveChanges on the ObjectContext.

That’s it folks. I left out a few of the factor interfaces as they are just boring.

Tags: , ,

Programming

Comments

8/18/2010 12:04:27 AM #

Valdimar Thor

Nice article Mark, I would love to see the whole solution to get a better idea how this works.

Valdimar Thor Iceland | Reply

Add comment


(Will show your Gravatar icon)

  Country flag


  • Comment
  • Preview
Loading



Copyright © 2001-2012 MS Consulting, Inc. All Rights Reserved.