Wednesday 18 December 2013

Unit Of Work Abstraction For NHibernate or Entity Framework C# Example

Unit Of Work is another great pattern that every software engineer needs to have in his tool box. So what's a purpose of Unit Of Work?

 “Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.” – Patterns of enterprise application architecture, Martin Fowler

Sounds like a lot of work, managing object state, relationships, concurrency and transactions can be cumbersome. However we are in luck, modern ORM's like NHibernate and Entity Framework support implicit Unit Of Work. In NHibernate it’s part of the ISession and in Entity Framework it’s part of the Context.

NHibernate
using(ISession session = sessionFactory.OpenSession()) 
using(ITransaction tx = session.BeginTransaction()) 
{ 
        Customer customer = new Customer { FirstName = "Peter" };
        session.Save(customer);
        tx.Commit(); 
} 

Entity Framework
using (SomeContext context = new SomeContext())
{
    Customer customer = new Customer { FirstName = "Peter" };
    context.Customer.Add(customer);
    context.SaveChanges();
}

ORM's have built in Unit Of Work, ideally these need to be abstracted away. Abstraction will allow us to move to a different ORM platform in the future and yield cleaner architecture. Lets take a look at the abstracted example:
Customer customer = new Customer { FirstName = "Peter"};
this.repositoryCustomer.Add(customer);
this.unitOfWork.Commit();

When it comes to working with databases it's important to read and write within a transaction scope. This ensures that there are no data inconsistencies, performance issues and second level cache is used. Please read this article for more information. So we need to open a transaction, but when do we do it?
  1. Open transaction manually in the service layer - Default Unit Of Work interface doesn't support .BeginTransaction, I don't believe it should be controlled by the user. Although this is a clean option.
  2. Open transaction on the page context creation - This feels dirty, each time some one hits your website your ORM will open a connection and begin a transaction. Not great for performance and is dangerous. 
  3. Open transaction in the repository on the first read or write - Best option, it's automatic, and the only down side is that there will be transaction null check each time (singleton). But you get cleaner service implementation.
In this article I am going to be focusing on point 3.

NHibernate Abstraction

Configuration Singleton
public static class NHConfigurationSingleton
    {
        private static Configuration configuration = null;
        private static object lockObj = new object();

        public static Configuration Configuration
        {
            get
            {
                lock (lockObj)
                {
                    if (configuration == null)
                    {
                        string[] resourceNames;
                        string nHResource = string.Empty;
                        Assembly[] asmArray = AppDomain.CurrentDomain.GetAssemblies();

                        foreach (Assembly asm in asmArray)
                        {
                            resourceNames = asm.GetManifestResourceNames();
                            nHResource = resourceNames.FirstOrDefault(x => x.ToLower().Contains("hibernate.config"));
                            if (!string.IsNullOrEmpty(nHResource))
                            {
                                using (Stream resxStream = asm.GetManifestResourceStream(nHResource))
                                {
                                    configuration = new Configuration();
                                    configuration.Configure(new XmlTextReader(resxStream));
                                }
                            }
                        }
                    }
                }

                return configuration;
            }
        }
    }

Session Factory Singleton
    public static class NHSessionFactorySingleton
    {
        private static ISessionFactory sessionFactory = null;
        private static object lockObj = new object();

        public static ISessionFactory SessionFactory
        {
            get
            {
                lock (lockObj)
                {
                    if (sessionFactory == null)
                    {
                        sessionFactory = NHConfigurationSingleton.Configuration.BuildSessionFactory();
                    }
                }

                return sessionFactory;
            }
        }
    }


Unit Of Work
    
public interface IUnitOfWork : IDisposable
    {
        void Commit();
        void Rollback();
    }

public class NHUnitOfWork : IUnitOfWork
    {
        private ISession session;
        private ITransaction transaction;
        public ISession Session { get { return this.session; } }

        public NHUnitOfWork() { }

        public void OpenSession()
        {
            if (this.session == null || !this.session.IsConnected)
            {
                if (this.session != null)
                    this.session.Dispose();

                this.session = NHSessionFactorySingleton.SessionFactory.OpenSession();
            }
        }

        public void BeginTransation(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
        {
            if (this.transaction == null || !this.transaction.IsActive)
            {
                if (this.transaction != null)
                    this.transaction.Dispose();

                this.transaction = this.session.BeginTransaction(isolationLevel);
            }
        }

        public void Commit()
        {
            try
            {
                this.transaction.Commit();
            }
            catch
            {
                this.transaction.Rollback();
                throw;
            }
        }

        public void Rollback()
        {
            this.transaction.Rollback();
        }

        public void Dispose()
        {
            if (this.transaction != null)
            {
                this.transaction.Dispose();
                this.transaction = null;
            }

            if (this.session != null)
            {
                this.session.Dispose();
                session = null;
            }
        }
    }

Repository
  
public class NHRepository<TEntity> : IRepoistory<TEntity>
        where TEntity : IDomainEntity
    {
        readonly NHUnitOfWork nhUnitOfWork;

        public NHRepository(NHUnitOfWork nhUnitOfWork)
        {
            this.nhUnitOfWork = nhUnitOfWork;
        }

        public void Add(TEntity entity)
        {
            this.nhUnitOfWork.OpenSession();
            this.nhUnitOfWork.BeginTransation();
            this.nhUnitOfWork.Session.Save(entity);
        }

        public void Remove(TEntity entity)
        {
            this.nhUnitOfWork.OpenSession();
            this.nhUnitOfWork.BeginTransation();
            this.nhUnitOfWork.Session.Delete(entity);
        }

        public void Update(TEntity entity)
        {
            this.nhUnitOfWork.OpenSession();
            this.nhUnitOfWork.BeginTransation();
            this.nhUnitOfWork.Session.Update(entity);
        }
        
        public IEnumerable<TEntity> GetAll()
        {
            this.nhUnitOfWork.OpenSession();
            this.nhUnitOfWork.BeginTransation();
            return this.nhUnitOfWork.Session.Query<TEntity>();
        }
    }

Entity Framework Abstraction

Unit Of Work
 
   public class EFUnitOfWork : IUnitOfWork
    {
        DbContext context;
        DbContextTransaction transaction;
        public DbContext Context { get { return this.context; } }

        public EFUnitOfWork() 
        { 
            this.context = new ECommerceEntities();
        }

        public void OpenTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
        {
            if (this.transaction == null)
            {
                if (this.transaction != null)
                    this.transaction.Dispose();

                this.transaction = this.context.Database.BeginTransaction(isolationLevel);
            }
        }

        public void Commit()
        {
            try
            {
                this.context.SaveChanges();
                this.transaction.Commit();
            }
            catch
            {
                this.transaction.Rollback();
                throw;
            }
        }

        public void Rollback()
        {
            this.transaction.Rollback();
        }

        public void Dispose()
        {
            if(this.transaction != null)
            {
                this.transaction.Dispose();
                this.transaction = null;
            }

            if(this.context != null)
            {
                this.context.Database.Connection.Close();
                this.context.Dispose();
                this.context = null;
            }
        }
    }

Repository
 
 public class EFRepository<TEntity> : IRepoistory<TEntity>
        where TEntity : class, IDomainEntity
    {
        readonly EFUnitOfWork unitOfWork;

        public EFRepository(EFUnitOfWork unitOfWork)
        {
            this.unitOfWork = unitOfWork;
        }

        public void Add(TEntity entity)
        {
            this.unitOfWork.OpenTransaction();
            this.unitOfWork.Context.Set<TEntity>().Add(entity);
        }

        public void Remove(TEntity entity)
        {
            this.unitOfWork.OpenTransaction();
            this.unitOfWork.Context.Set<TEntity>().Remove(entity);
        }

        public IEnumerable<TEntity> GetAll()
        {
            this.unitOfWork.OpenTransaction();
            return this.unitOfWork.Context.Set<TEntity>().ToList();
        }
    }

Example usage (see SQL trace output below)
    
public class CustomerService
    {
        readonly IUnitOfWork unitOfWork;
        readonly IRepoistory<Customer> repositoryCustomer;

        public CustomerService(IUnitOfWork unitOfWork, IRepoistory<Customer> repositoryCustomer)
        {
            this.unitOfWork = unitOfWork;
            this.repositoryCustomer = repositoryCustomer;
        }

  public CustomerDto Add(string firstName, string lastName)
        {
            int existingWithSameName = this.repositoryCustomer.GetAll()
                    .Count(customer => customer.FirstName == firstName && customer.LastName == lastName);

            if (existingWithSameName != 0)
                throw new Exception("User already exists with this name");

            Customer customerNew = new Customer(firstName, lastName);
            
            this.repositoryCustomer.Add(customerNew);
            this.unitOfWork.Commit();

            return new CustomerDto(customerNew.Id, customerNew.FirstName, customerNew.LastName);
        }

    }

NHibernate SQL Trace Output
To see exactly what's going on i've decided to go through SQL logs. It seems to behave correctly, it connects, starts a transaction, performs required operations, commits transaction and closes all connections.
2013-12-17 23:24:24.09 spid51      ODS Event: Login connection 51
2013-12-17 23:24:24.09 spid51      ODS Event: Remote_ods : Xact 0 ORS#: 1, connId: 0
2013-12-17 23:24:24.09 spid51      Xact BEGIN for Desc: 3300000001
2013-12-17 23:24:25.19 spid51      ODS Event: language_exec : Xact 3300000001 ORS#: 1, connId: 0
2013-12-17 23:24:25.19 spid51         Text:select customer0_.Id as Id0_, customer0_.FirstName as FirstName0_, customer0_.LastName as LastName0_ from ECommerce.dbo.Customer customer0_
2013-12-17 23:24:25.35 spid51      Parameter# 0: Name=,Flags=0,Xvt=231,MaxLen=166,Len=166,Pxvar Value=INSERT INTO ECommerce.dbo.Customer (FirstName, LastName, Id) VALUES (@p0, @p1, @p2)
2013-12-17 23:24:25.36 spid51      Parameter# 1: Name=,Flags=0,Xvt=231,MaxLen=116,Len=116,Pxvar Value=@p0 nvarchar(4000),@p1 nvarchar(4000),@p2 uniqueidentifier
2013-12-17 23:24:25.36 spid51      Parameter# 2: Name=@p0,Flags=0,Xvt=231,MaxLen=8000,Len=10,Pxvar Value=ZanNH
2013-12-17 23:24:25.36 spid51      Parameter# 3: Name=@p1,Flags=0,Xvt=231,MaxLen=8000,Len=14,Pxvar Value=70ba671
2013-12-17 23:24:25.36 spid51      Parameter# 4: Name=@p2,Flags=0,Xvt=36,MaxLen=16,Len=16,Pxvar Value=E5EF2FD7-532B-4CEA-80F4-8B095D1F983A
2013-12-17 23:24:25.36 spid51          IPC Name: sp_executesql
2013-12-17 23:24:25.36 spid51      ODS Event: execrpc : Xact 3300000001 ORS#: 1, connId: 0
2013-12-17 23:24:25.36 spid51      ODS Event: Remote_ods : Xact 3300000001 ORS#: 1, connId: 0
2013-12-17 23:24:25.36 spid51      Xact COMMIT for Desc: 3300000001
2013-12-17 23:24:27.71 Server      ODS Event: Exit spid 51
2013-12-17 23:24:27.71 spid51      ODS Event: Logout connection 51
2013-12-17 23:24:27.71 Server      ODS Event: Close conn 0 on spid 51

Entity Framework SQL Trace Output
2013-12-17 23:27:06.21 spid51      ODS Event: Login connection 51
2013-12-17 23:27:06.22 Server      Connection 0x00000001EE6433F0 connId:1, spid:51     New MARS Session 
2013-12-17 23:27:06.22 spid51      ODS Event: Remote_ods : Xact 0 ORS#: 1, connId: 1
2013-12-17 23:27:06.22 spid51      Xact BEGIN for Desc: 3300000001
2013-12-17 23:27:08.06 Server      Connection 0x00000001F2F317A0 connId:2, spid:51     New MARS Session 
2013-12-17 23:27:08.06 spid51      ODS Event: language_exec : Xact 3300000001 ORS#: 1, connId: 2
2013-12-17 23:27:08.06 spid51         Text:SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[LastName] AS [LastName]
    FROM [dbo].[Customer] AS [Extent1]
2013-12-17 23:27:08.56 spid51      Parameter# 0: Name=,Flags=0,Xvt=231,MaxLen=154,Len=154,Pxvar Value=INSERT [dbo].[Customer]([Id], [FirstName], [LastName])
VALUES (@0, @1, @2)

2013-12-17 23:27:08.56 spid51      Parameter# 1: Name=,Flags=0,Xvt=231,MaxLen=102,Len=102,Pxvar Value=@0 uniqueidentifier,@1 varchar(256),@2 varchar(256)
2013-12-17 23:27:08.56 spid51      Parameter# 2: Name=@0,Flags=0,Xvt=36,MaxLen=16,Len=16,Pxvar Value=4318564C-304D-4792-85C5-F0113A873700
2013-12-17 23:27:08.56 spid51      Parameter# 3: Name=@1,Flags=0,Xvt=167,MaxLen=256,Len=5,Pxvar Value=ZanEF
2013-12-17 23:27:08.56 spid51      Parameter# 4: Name=@2,Flags=0,Xvt=167,MaxLen=256,Len=7,Pxvar Value=2c22266
2013-12-17 23:27:08.56 spid51          IPC Name: sp_executesql
2013-12-17 23:27:08.56 spid51      ODS Event: execrpc : Xact 3300000001 ORS#: 1, connId: 2
2013-12-17 23:27:08.58 spid51      ODS Event: Remote_ods : Xact 3300000001 ORS#: 1, connId: 1
2013-12-17 23:27:08.58 spid51      Xact COMMIT for Desc: 3300000001
2013-12-17 23:27:08.61 Server      ODS Event: Close conn 1 on spid 51
2013-12-17 23:27:08.61 Server      ODS Event: Close conn 2 on spid 51
2013-12-17 23:27:08.61 Server      ODS Event: Exit spid 51
2013-12-17 23:27:08.61 spid51      ODS Event: Logout connection 51
2013-12-17 23:27:08.61 Server      ODS Event: Close conn 0 on spid 51


We can pass NHUnitOfWork or EFUnitOfWork in to CustomerService and it will work as expected. Do avoid calling Unit Of Work within the Repository i.e. this.repositoryCustomer.Commit(), I've seen this a lot around the web. Unit Of Work should not be bound directly to it.

*Note: Code in this article is not production ready and is used for prototyping purposes only. If you have suggestions or feedback please do comment.


References: 

11 comments:

  1. Nice article, but how are you ensuring that the same unit of work instance is being used in your repository to begin the transaction and in your service to commit the transaction?

    ReplyDelete
    Replies
    1. Thanks for Reading!

      Are you asking how do I control the life cycle of the unit of work in general or how do I use the same instance of the unit of work together with NHibernate and the Entity Framework?

      Delete
    2. I was meaning the life cycle of the unit of work, in my case I used Ninject to handle it.

      I've also just finished writing an article on how I implemented the same pattern with an MVC5 application, if you're interested it's at http://jasonwatmore.com/post/2015/01/28/Unit-of-Work-Repository-Pattern-in-MVC5-with-Fluent-NHibernate-and-Ninject.aspx

      Delete
    3. Great article, it's very easy to read.

      My only worry with your implementation is that it forces commit no matter what (as far as I understand). What if you are manipulating 3 different objects and on the last object there is a failure, you would want to catch that exception and rollback. Let me know If I have missed something.

      I use Castle Windsor Lifestyle "Per Web Request" which I am assuming is same as InRequestScope that you have used.


      Delete
    4. Thanks for the feedback.

      Yes I'm checking for errors and rolling back within the Commit() method of the UnitOfWork class just like your implementation above, I didn't see the need for a Rollback() method on the IUnitOfWork interface as well since it's all handled within Commit(), but let me know if I'm wrong.

      Yep I'd say the "Per Web Request" in Castle Windsor is the same as InRequestScope in Ninject.

      Delete
  2. Do you have any idea how I can implement UnitOfWork with multiple contexs?

    ReplyDelete
    Replies
    1. Thank you for the question!

      Question:
      How can I implement Entity Framework UnitOfWork with multiple context transactions?

      Answer:
      Here is a skeleton example (this example was not tested and has no error handling):

      public class EFUnitOfWorkContextA : EFUnitOfWork
      {
      //Init your DbContext and config your database
      }

      public class EFUnitOfWorkContextB : EFUnitOfWork
      {
      //Init your DbContext and config your database
      }

      public class EFUnitOfWorkMultiContext : IUnitOfWork
      {
      EFUnitOfWorkContextA a;
      EFUnitOfWorkContextB b;
      TransactionScope scope;

      public EFUnitOfWorkMultiContext(EFUnitOfWorkContextA a, EFUnitOfWorkContextB b)
      {
      this.a = a;
      this.b = b;
      }

      public void OpenTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
      {
      scope = new TransactionScope();
      this.a.OpenTransaction(isolationLevel);
      this.b.OpenTransaction(isolationLevel);
      }

      public void Commit()
      {
      try
      {
      this.a.Commit();
      this.b.Commit();
      this.scope.Complete();
      }
      catch
      {
      this.Rollback();
      throw;
      }
      }

      public void Rollback()
      {
      this.a.Rollback();
      this.b.Rollback();
      }

      public void Dispose()
      {
      this.a.Dispose();
      this.b.Dispose();
      this.scope.Dispose();
      }
      }

      This solution uses Decorator pattern to extend your existing interfaces and implementations. You will need to register EFUnitOfWorkContextA and EFUnitOfWorkContextB with Dependency Injection, so when you resolve EFUnitOfWorkMultiContext it will automatically resolve A and B.

      Also you might find the following useful:
      http://stackoverflow.com/questions/2370434/how-to-run-two-entity-framework-contexts-inside-transactionscope-without-msdtc

      http://stackoverflow.com/questions/2787408/not-use-using-statement-for-transactionscope

      I hope this helps!

      Delete
    2. Thank you so much, works well. :)

      Delete
    3. That's great, thank you for the feedback!

      Delete
  3. Hi Zan,

    Nice Article, Can you please share some example where we modify two levels of data (Parent + Related Child data) instead of simple Customer level properties.

    ReplyDelete
  4. Hi Zan,

    Nice article. I am wondering what the reason for this statement "Do avoid calling Unit Of Work within the Repository". Could you kindly explain the reason, please ? Thanks

    ReplyDelete