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:
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:
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:
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:
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:
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:
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:
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();
}
public static void Do(Action<UnitOfWork> work, UnitOfWorkSettings settings = null)
{
Requires.NotNull(work, "work");
using (var uow = new UnitOfWork(settings))
{
work(uow);
}
}
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);
}
}
public void Dispose()
{
if (Marshal.GetExceptionCode() == 0)
{
if (Settings.RollbackOnDispose)
Rollback();
else
Commit();
}
else
{
if (IsRoot && !IsFinished)
CloseUnitOfWork();
}
}
public void Commit()
{
Perform(() =>
{
if (Settings.EnableCommit)
SessionScope.Commit();
});
}
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;
}
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:
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:
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:
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 NHibernate, EntityFramework, Linq2Db, Castle Windsor, StructureMap.
Previous article(Part |)