Click here to Skip to main content
15,886,518 members
Articles / Web Development / HTML

Antler: Abstraction over ORM that you like to use in .NET(Part II)

Rate me:
Please Sign up or sign in to vote.
4.89/5 (6 votes)
6 Aug 2014CPOL3 min read 13.6K   92   7   1
Using the same syntax to work with different ORMs. Diving into details of Antler framework.

Introduction

In the previous article I introduced Antler framework to the CodeProject community with high-level overview.
Now I want to dive under the hood to show you some details of how this framework was designed.

Framework implemented in a pluggable way and consists of one Antler.Core library and many adapters. Core library contains all necessary abstractions and shared functionallity, whereas adapter libraries contain specific impementations for different ORMs and IoC containers.

This pluggable structure allows to switch easily between different ORMs/Databases and IoC containers in you project and to use a common habitual syntax to work with them.

Currently there are NHibernate, EntityFramework, Linq2Db, Castle Windsor, StructureMap adapters available via NuGet.

In this article we'll dive into details of implementing Unit-of-work and ORM adapter for Antler framework, using Antler.NHibernate adapter as an example.

Usage examples

All work with a database is performed via UnitOfWork class which represents an underlying database transaction.

For example, UnitOfWork with multiple operations:

C#
UnitOfWork.Do(uow => 
               { 
                var hasHockeyTeams = uow.Repo<Team>().AsQueryable().
                                                      Any(t => t.Description == "Hockey");
                if (!hasHockeyTeams) 
                { 
                 uow.Repo<Team>().Insert(new Team() {Name = "Penguins", Description = "Hockey"});
                 uow.Repo<Team>().Insert(new Team() {Name = "Capitals", Description = "Hockey"}) 
                }});

UnitOfWork with simple query:

C#
var hockeyTeams = UnitOfWork.Do(uow => uow.Repo<Team>().AsQueryable().
                                                        Where(t => t.Description == "Hockey").
                                                        ToList());

UnitOfWork is fully configurable. We could configure behavior of UnitOfWork on an application level in a bootstrapper. For example, let's configure an application that uses Castle Windsor container and NHibernate + Oracle storage to perform Rollback instead of Commit in the end of any UnitOfWork:

C#
var configurator = new AntlerConfigurator();
configurator.UseWindsorContainer()
            .UseStorage(NHibernateStorage.Use.WithDatabaseConfiguration(
                        OracleDataClientConfiguration.Oracle10.
                        ConnectionString(Config.ConnectionString).
                        DefaultSchema(Config.DbSchemaName).ShowSql()).
                        WithMappings("Sport.Mappings")).
                        SetUnitOfWorkDefaultSettings(new UnitOfWorkSettings() 
                                                         { RollbackOnDispose = true });

And it is not necessary to do it on an application level, you could configure the specific UnitOfWork as well:

C#
UnitOfWork.Do(uow =>
              {
               var hasHockeyTeams = uow.Repo<Team>().AsQueryable().
                                                       Any(t => t.Description == "Hockey");
               if (!hasHockeyTeams)
               {
                uow.Repo<Team>().Insert(new Team() { Name = "Penguins", Description = "Hockey" });
                uow.Repo<Team>().Insert(new Team() { Name = "Capitals", Description = "Hockey" });
               }
              }, new UnitOfWorkSettings(){ RollbackOnDispose = true }); 

You could disable commits, throw exception if nested UnitOfWork detected, specify concrete storage to work with and other stuff via UnitOfWorkSettings class. Things like this could be useful in testing projects. Because, unfortunately, sometimes in enterprise projects we can't generate testing database to run tests over. So, options like RollbackOnDispose might be very handy.

Another thing worth to tell is that we could configure our application to have multiple storages to allow data transfer between them. For example:

var configurator = new AntlerConfigurator();
configurator.UseBuiltInContainer()
            .UseStorage(EntityFrameworkStorage.Use.
                        WithConnectionString("Data Source=.\\SQLEXPRESS;
                        Initial Catalog=Database1;Integrated Security=True").WithLazyLoading().
                        WithDatabaseInitializer(
                        new DropCreateDatabaseIfModelChanges<DataContext>()).
                        WithMappings(Assembly.Load("Blog.Mappings.EF")), "Store1")
            .UseStorage(EntityFrameworkStorage.Use.
                        WithConnectionString("Data Source=.\\SQLEXPRESS;
                        Initial Catalog=Database2;Integrated Security=True").
                        WithMappings(Assembly.Load("Blog.Mappings.EF")), "Store2");

Now we could work with the 2 storages in our application. For example, let's transfer Employee information from one storage into another:

C#
var userFromSourceStorage = UnitOfWork.Do(uow => uow.Repo<Employee>().GetById(gpin), 
                                         new UnitOfWorkSettings {StorageName = "Store1"}); 

UnitOfWork.Do(uow => 
                { 
                  var foundUserInCurrentStorage = uow.Repo<Employee>().GetById(gpin); 
                  if (foundUserInCurrentStorage == null) 
                   {
                     uow.Repo<Employee>().Insert(userFromSourceStorage);
                   } 
                 }, new UnitOfWorkSettings{StorageName = "Store2"});

Preferable way to work with UnitOfWork is to use a lambda expression specified in the Do method. But sometimes we may need to get current UnitOfWork like this:

C#
var hasHockeyTeams = UnitOfWork.Current.Value.Repo<Team>().AsQueryable().
                                                           Any(t=>t.Description == "Hockey");
if (!hasHockeyTeams)
 {
   UnitOfWork.Current.Value.Repo<Team>().Insert(new Team() 
                                                  { Name = "Penguins", Description = "Hockey" });
   UnitOfWork.Current.Value.Repo<Team>().Insert(new Team() 
                                                  { Name = "Capitals", Description = "Hockey" });
  }

We could do it because we use a thread static field to keep UnitOfWork on a thread level. Of course, in this example good practice would be to check if we have UnitOfWork in our current context, before using it.

Implementation details

Let's look at UnitOfWork class:
C#
public class UnitOfWork: IDisposable
    {
        public ISessionScope SessionScope { get; private set; }

        [ThreadStatic]
        private static UnitOfWork _current;
        public static Option<UnitOfWork> Current
        {
            get { return _current.AsOption(); }            
        }

        public bool IsFinished
        {
            get { return _current == null; }
        }
        
        public bool IsRoot { get; private set; }
                
        public Guid Id { get; private set; }
        public UnitOfWorkSettings Settings { get; private set; }
        
        public static Func<string, ISessionScopeFactory> SessionScopeFactoryExtractor 
                                                                        { get; set; }
                        
        private UnitOfWork(UnitOfWorkSettings settings)
        {
            Settings = settings ?? UnitOfWorkSettings.Default;

            Assumes.True(SessionScopeFactoryExtractor != null, "SessionScopeFactoryExtractor 
                                                               should be set before using 
                                                               UnitOfWork. Wrong configuraiton?");
            
            Assumes.True(!string.IsNullOrEmpty(Settings.StorageName), "Storage name can't be null 
                                                                       or empty. 
                                                                       Wrong configuration?");    
            
            var sessionScopeFactory = SessionScopeFactoryExtractor(Settings.StorageName);
            
            Assumes.True(sessionScopeFactory != null, "Can't find storage with name {0}. 
                                                       Wrong storage name?",Settings.StorageName);
            SetSession(sessionScopeFactory);
        }
                
        private void SetSession(ISessionScopeFactory sessionScopeFactory)
         {
            Requires.NotNull(sessionScopeFactory, "sessionScopeFactory");
            
             if (_current == null)
             {
                 SessionScope = sessionScopeFactory.Open();
                 IsRoot = true;                 
             }
             else
             {
                 if (Settings.ThrowIfNestedUnitOfWork)
                     throw new NotSupportedException("Nested UnitOfWorks are not 
                                                      supported due to UnitOfWork Settings 
                                                      configuration");

                 SessionScope = _current.SessionScope;
                 IsRoot = false;
             }
                                                  
            _current = this;            
            Id = Guid.NewGuid();            
         }

        /// <summary>
        /// Start database transaction.
        /// </summary>   
        public static void Do(Action<UnitOfWork> work, UnitOfWorkSettings settings = null)
        {
            Requires.NotNull(work, "work");
            
            using (var uow = new UnitOfWork(settings))
            {
                work(uow);
            }
        }
        
        /// <summary>
        /// Start database transaction and return result from it.
        /// </summary>   
        public static TResult Do<TResult>(Func<UnitOfWork, TResult> work, UnitOfWorkSettings 
       settings = null)
        {
            Requires.NotNull(work, "work");

            using (var uow = new UnitOfWork(settings))
            {
                return work(uow);
            }
        }

        /// <summary>
        /// Commit/Rollback transaction(depending on the configuration) explicitly. Will be called
        /// automatically in the end of the "Do" block.
        /// </summary> 
        public void Dispose()        
        {
            if (Marshal.GetExceptionCode() == 0)
            {
                if (Settings.RollbackOnDispose)
                    Rollback();
                else
                    Commit();
            }
            else
            {
                if (IsRoot && !IsFinished)
                  CloseUnitOfWork();
            }
        }

        /// <summary>
        /// Commit database transaction explicitly(not necessarily to use in standard 
        /// configuration, because transaction will be committed anyway in the end of the 
        /// "Do" block).
        /// </summary>  
        public void Commit()
        {                        
                Perform(() =>
                    {
                        if (Settings.EnableCommit) 
                            SessionScope.Commit();
                    });
        }

        /// <summary>
        /// Rollback database transaction explicitly. As alternative you can use 
        /// RollbackOnDispose setting to rollback transaction automatically in the end of the "Do"
        /// block(may be useful in testing).
        /// </summary>  
        public void Rollback()
        {
            Perform(() => SessionScope.Rollback());            
        }

        private void Perform(Action action)
        {
            Requires.NotNull(action, "action");
            if (IsRoot && !IsFinished)
            {
                try
                {
                    action();
                }
                finally 
                {
                    CloseUnitOfWork();
                }                                
            }
        }

        private void CloseUnitOfWork()
        {            
            SessionScope.Dispose();
            _current = null;                           
        }

        /// <summary>
        /// Get Repository object to perform queries/operations on database.
        /// </summary> 
        public IRepository<TEntity> Repo<TEntity>() where TEntity: class
        {
            return SessionScope.CreateRepository<TEntity>();
        }                   
    }

UnitOfWork class resides in Antler.Core library(which has not NuGet dependencies), so UnitOfWork needs to be fully decoupled from concrete ORM implementations. Usially right way to inject dependencies is to use constructor, but in this case we don't want to do it every time we create UnitOfWork. So, concrete ISessionScopeFactory(see example below) dependency comes through into UnitOfWork class via static property, as a result of the fluent configuration shown above.

ISessionScopeFactory implementation has single Open method which is called to create concrete implementation of ISessionScope at the beginning of UnitOfWork. ISessionScopeFactory implementation for NHibernate looks like:

C#
public class NHibernateSessionScopeFactory: ISessionScopeFactory, ISessionScopeFactoryEx
    {
        private readonly ISessionFactory _sessionFactory;
        private ISession _session;
        
        public NHibernateSessionScopeFactory(ISessionFactory sessionFactory)
        {
            Requires.NotNull(sessionFactory, "sessionFactory");            
            _sessionFactory = sessionFactory;
        }
        
        public ISessionScope Open()
        {            
            if (_session == null)
              return new NHibernateSessionScope(_sessionFactory);

            return new NHibernateSessionScope(_session);
        }

        void ISessionScopeFactoryEx.SetSession(ISession session)
        {
            Requires.NotNull(session, "session");            
            _session = session;
        }

        void ISessionScopeFactoryEx.ResetSession()
        {
            _session = null;
        }
    }

The only remarkable thing here is that we allow to set /reset session explicitly. But this option is rarely used in applications directly - mostly in testing projects, when you need to keep single session between multiple UnitOfWorks e.g. when writing Integration tests using in-memory database(Sqlite).

ISessionScope implementation for NHibernate looks like:

C#
public class NHibernateSessionScope: ISessionScope
    {
        private readonly ISession _session;
        private readonly ITransaction _transaction;
        private readonly bool _ownSession;
        
        public NHibernateSessionScope(ISessionFactory sessionFactory)
        {                        
            Requires.NotNull(sessionFactory, "sessionFactory");
            
            _session = sessionFactory.OpenSession();            
            _transaction = _session.BeginTransaction();
            _ownSession = true;
        }

        public NHibernateSessionScope(ISession session)
        {
            Requires.NotNull(session, "session");
            
            _session = session;
            _transaction = _session.BeginTransaction();
            _ownSession = false;
        }

        public void Commit()
        {
            AssertIfDone();
            try
            {
                _transaction.Commit();
            }
            catch (HibernateException)
            {
                _transaction.Rollback();
                throw;
            }            
        }
        
        public void Rollback()
        {
            AssertIfDone();
            _transaction.Rollback();
        }
        
        private void AssertIfDone()
        {
            Assumes.True(!_transaction.WasCommitted, "Transaction already was commited");
            Assumes.True(!_transaction.WasRolledBack, "Transaction already was rolled back");
        }

        public IRepository<TEntity> CreateRepository<TEntity>() where TEntity:class
        {
            return new NHibernateRepository<TEntity>(_session);
        }

        public TInternal GetInternal<TInternal>() where TInternal : class
        {
           var internalSession = _session as TInternal;
           Assumes.True(internalSession != null, "Can't cast Internal Session to TInternal type");
           return internalSession;
        }

        public void Dispose()
        {                        
            _transaction.Dispose();            
            if (_ownSession)                            
              _session.Dispose();                            
        }       
    }

Concrete ISessionScope implementation is actually a wrapper around an underlying ORM's session which is used by UnitOfWork to create, commit, rollback transaction and to get IRepository implementation for the specific ORM. Plus you could dig down to get internal ORM's session as a way to perform some specific ORM operations not supported by the unified Antler syntax.

IRepository implementation for NHibernate looks like:

C#
public class NHibernateRepository<TEntity>: IRepository<TEntity> where TEntity: class
    {
        private readonly ISession _session;
        public NHibernateRepository(ISession session)
        {
            Requires.NotNull(session, "session");
            _session = session;
        }

        public virtual IQueryable<TEntity> AsQueryable()
        {
            return _session.Query<TEntity>();
        }

        public TEntity GetById<TId>(TId id)
        {
            return _session.Get<TEntity>(id);  
        }
        
        public TEntity Insert(TEntity entity)
        {
            Requires.NotNull(entity, "entity");
            _session.Save(entity);
            return entity;
        }

        public TId Insert<TId>(TEntity entity)
        {
            Requires.NotNull(entity, "entity");
            Requires.True(typeof(TId).IsValueType, "Only value type Ids are 
                                                    supported(int, decimal etc.)");

            return (TId)_session.Save(entity);
        }

        public TEntity Update(TEntity entity)
        {
            Requires.NotNull(entity, "entity");
            return _session.Merge(entity);
        }

        public void Delete(TEntity entity)
        {
            Requires.NotNull(entity, "entity");
            _session.Delete(entity);
        }

        public void Delete<TId>(TId id)
        {
            var entity = GetById(id);
            if (entity != null)
            {
                _session.Delete(entity);
            }            
        }        
    }

IRepository implementation allows to perform standart set of operations via ORM's session.

Conclusion

If you are interested, you could find adapter implementations for NHibernate, EntityFramework and Linq2Db on GitHub.

If you want to implement adapter for another(your own?) ORM or IoC container that is not supported by Antler yet, please, be my guest.

Or you could just install the framework from NuGet:

Core library, and adapters for NHibernateEntityFramework,  Linq2DbCastle WindsorStructureMap.

Previous article(Part |)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead UBS
Russian Federation Russian Federation
Currently Technical Lead at UBS

Comments and Discussions

 
GeneralMy vote of 5 Pin
johannesnestler6-Aug-14 0:44
johannesnestler6-Aug-14 0:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.