Enhanced UnitOfWork Implementation

Sep 13, 2009 at 1:42 AM

Tim,

Your book has provided much inspiration and shown practical applications of Evan's work.  The last few months has been really fun for me, and now my team is also starting to work with DDD and catching the bug, as well.  I have made a modification to the UnitOfWork.cs implementation; actually it is mostly a rewrite.  I wanted to share it with you and everyone else, in case in comes in handy.

The inspiration was that the UOW felt arbitrary, in the order that it actually persisted the entities.  When starting to use (a modified version of) the code in the book, I ran into a couple of issues where I felt that the UOW would work better for my purposes if it was sequentially executed.  I ran into this when I wanted to use a single UOW inside a Domain Service that coordinated calls between two different Repository implementations.  The order that I needed some of the database operations to happen in were the same order by which the code called them.

So, this is what I came up with:

    /// <summary>
    /// Provides a distinct unit of work to be conducted for a type of entity.
    /// </summary>
    public sealed class UnitOfWork : IUnitOfWork
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="UnitOfWork"/> class.
        /// </summary>
        public UnitOfWork()
        {
            this.Operations = new List<Operation>();
        }

        #endregion

        #region Enumerations

        /// <summary>
        /// Provides the type of persistence operation to run.
        /// </summary>
        private enum OperationType
        {
            /// <summary>
            /// Operation for an add.
            /// </summary>
            Add,

            /// <summary>
            /// Operation for a delete.
            /// </summary>
            Delete,

            /// <summary>
            /// Operation for an update.
            /// </summary>
            Update
        }

        #endregion

        #region Private Property Declarations

        /// <summary>
        /// Gets or sets the operations.
        /// </summary>
        /// <value>The operations in this unit of work.</value>
        private List<Operation> Operations { get; set; }

        #endregion

        #region Public Methods

        /// <summary>
        /// Commits all batched changes within the scope of a <see cref="TransactionScope" />.
        /// </summary>
        public void Commit()
        {
            using (var scope = new TransactionScope())
            {
                foreach (var operation in this.Operations.OrderBy(o => o.ProcessDate))
                {
                    switch (operation.Type)
                    {
                        case OperationType.Add:
                            operation.Repository.PersistNewItem(operation.Entity);
                            break;
                        case OperationType.Delete:
                            operation.Repository.PersistDeletedItem(operation.Entity);
                            break;
                        case OperationType.Update:
                            operation.Repository.PersistUpdatedItem(operation.Entity);
                            break;
                    }
                }

                scope.Complete();
            }

            this.Operations.Clear();
        }

        /// <summary>
        /// Registers an <see cref="IEntity" /> instance to be added through this <see cref="UnitOfWork" />.
        /// </summary>
        /// <param name="entity">The <see cref="IEntity" />.</param>
        /// <param name="repository">The <see cref="IUnitOfWorkRepository" /> participating in the transaction.</param>
        public void RegisterAdded(IEntity entity, IUnitOfWorkRepository repository)
        {
            this.Operations.Add(
                new Operation
                    {
                        Entity = entity, 
                        ProcessDate = DateTime.Now, 
                        Repository = repository, 
                        Type = OperationType.Add 
                    });
        }

        /// <summary>
        /// Registers an <see cref="IEntity" /> instance to be changed through this <see cref="UnitOfWork" />.
        /// </summary>
        /// <param name="entity">The <see cref="IEntity" />.</param>
        /// <param name="repository">The <see cref="IUnitOfWorkRepository" /> participating in the transaction.</param>
        public void RegisterChanged(IEntity entity, IUnitOfWorkRepository repository)
        {
            this.Operations.Add(
                new Operation
                    {
                        Entity = entity,
                        ProcessDate = DateTime.Now,
                        Repository = repository,
                        Type = OperationType.Update
                    });
        }

        /// <summary>
        /// Registers an <see cref="IEntity" /> instance to be deleted through this <see cref="UnitOfWork" />.
        /// </summary>
        /// <param name="entity">The <see cref="IEntity" />.</param>
        /// <param name="repository">The <see cref="IUnitOfWorkRepository" /> participating in the transaction.</param>
        public void RegisterDeleted(IEntity entity, IUnitOfWorkRepository repository)
        {
            this.Operations.Add(
                new Operation
                {
                    Entity = entity,
                    ProcessDate = DateTime.Now,
                    Repository = repository,
                    Type = OperationType.Delete
                });
        }

        #endregion

        /// <summary>
        /// Provides a snapshot of an entity and the repository reference it belongs to.
        /// </summary>
        private sealed class Operation
        {
            /// <summary>
            /// Gets or sets the entity.
            /// </summary>
            /// <value>The entity.</value>
            public IEntity Entity { get; set; }

            /// <summary>
            /// Gets or sets the process date.
            /// </summary>
            /// <value>The process date.</value>
            public DateTime ProcessDate { get; set; }

            /// <summary>
            /// Gets or sets the repository.
            /// </summary>
            /// <value>The repository.</value>
            public IUnitOfWorkRepository Repository { get; set; }

            /// <summary>
            /// Gets or sets the type of operation.
            /// </summary>
            /// <value>The type of operation.</value>
            public OperationType Type { get; set; }
        }
    }

The general concept is that instead of storing the three types of operations separately and going through the lists in a grouped order, I would contain them all with a single type of class, the private Operation class.  It simply tracks the same information that the old Dictionary<> instances did, but adds a DateTime and an OperationType enumeration value.  Instead of three loops (one for each of the previous operation type), it is replaced by a single loop with a switch() statement.  The List<Operation> instance is just ordered by the DateTime and executed in order.

Two things are missing - well, one is missing, and one is my personal preference.  The first, is that I don't have the ClientTransactionRepository implementation in here, as I had removed it from my local implementation.  It should be very easy to add it back in, though.  The second is that the UOW has an explicit Commit() method, but no explicit way to execute what is the UOW with an implied rollback (thinking about unit testing here, where I can still go through the queue but keep the database pristine to get beyond testing with mocks - but that is another story for another day.)

Anyhow, I hope that someone finds this useful.  It is just nice to be able to give something back.

Thanks!

Joseph

Oct 24, 2009 at 6:37 PM

This is mostly about preferences, but there are a few things I don't like in your otherwise excellent code:

  • Regions. Most programmer's don't need them to recognize enums, constructors and public methods.
  • The use of xml comments. Most of them are stating the obvious and add nothing but noise.
  • You could benefit from polymorphism by having an AddOperation, DeleteOperation and UpdateOperation. Then you can skip the enum entirely.
  • You don't need ProcessDate. The List<> keeps the items in added order, which also is the execution order.

The Commit method can then be reduced to:

 

        public void Commit()
  {
using (var scope = new TransactionScope())
{
foreach (var operation in Operations)
{
operation.Execute();
}

scope.Complete();
}

this.Operations.Clear();
}

The Register methods can then be changed accordingly:

        public void RegisterAdded(IEntity entity, IUnitOfWorkRepository repository)
{
this.Operations.Add(
new AddOperation
{
Entity = entity,
Repository = repository,
});
}

Jan 15, 2010 at 8:09 AM

It is mostly about preferences.  As far as regions go, they are not needed.  However, I do not write them out by hand, either.  I use Regionerate to generate them.  I'll heartily disagree with the opinion about XML comments, though.  They may seem redundant in looking at the code, but I use them for what they are intended - generating documentation.  Part of the build process uses Sandcastle to generate help documentation for the development team.  As the code is used more, remarks get added to the comments, and the documentation becomes an important tool in day to day operations.  Considering the tools out there like GhostDoc, not to add them is lazy in my opinion.  They do not hurt anything and provide value to myself and my team - in terms of value added to the documentation.

I did go back and forth about enums vs separated method calls.  I don't have feelings one way or the other really.  The code it is called from is really boilerplate and does not need be change that often once set up.  As for the dates, though, I understand that they are FIFO in the List<> implementation - but I include those for auditing purposes.  Once again, not everyone would need them and they could be just as easily assigned from the SQL Server, or whatever other implementation is used.  It really depends on if the "modification" time in your realm is about when the user made the actual change or when the change was committed.

Coordinator
May 12, 2011 at 6:11 AM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.