Making the UI agnostic of chosen persistence engine

Yesterday, I set out to see if I could design a solution where the only project to reference any persistence engine libraries was the actual Persistence layer.

One of the reasons I even attempted this was in regards to current requirements to having a persistence boostrapper in the UI project (in this case MVC) to initialize FluentNHibernate.

In past Entity Framework projects, this kind of initialization wasn’t require. The Context defined in the persistence layer merely looked for a connection string in web.config or app.config.

It made sense to me that I should be able to do something similar with FluentNHibernate. I’ll describe how I did so below, along with some notes on the experience and practicality of it.


Bsically what I did was combine the UnitOfWork and Repository patterns. The following two interfaces were present in the AgnosticsTest.Domain.Persistence namespace instead of the usual repository interfaces:

using System;
using System.Collections.Generic;
using System.Linq;
namespace AgnosticsTest.Domain.Persistence
{
public interface IUnitOfWork : IDisposable
{
IQueryable<T> Select<T>() where T : Entity;
IQueryable<T> Select<T, I>() where T : Entity<I>;
T SelectById<T>(int id) where T : Entity;
T SelectById<T, I>(I id) where T : Entity<I>;
void Update(object obj);
}
}
using System.Data;
namespace AgnosticsTest.Domain.Persistence
{
public interface IUnitOfWorkFactory
{
IUnitOfWork Create(IsolationLevel isolationLevel = IsolationLevel.Unspecified);
}
}

While very basic, I merely wanted a proof of concept here. If I were to use this in production, I would certainly add more specific methods to the UnitOfWork interface. For now, this was all I needed.

I then added the following two implementations to the AgnosticsTest.Persistence library:

using System.Data;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
namespace AgnosticsTest.Persistence
{
using AgnosticsTest.Domain;
using AgnosticsTest.Domain.Persistence;
public class UnitOfWork : IUnitOfWork
{
private ISession session;
private ITransaction transaction;
public UnitOfWork(ISessionFactory sessionFactory, IsolationLevel isolationLevel = IsolationLevel.Unspecified)
{
session = sessionFactory.OpenSession();
transaction = session.BeginTransaction(isolationLevel);
}
public void Dispose()
{
try
{
transaction.Commit();
}
catch
{
transaction.Rollback();
}
finally
{
transaction.Dispose();
}
session.Close();
session.Dispose();
}
public IQueryable<T> Select<T>() where T : Entity
{
return session.Query<T>();
}
public IQueryable<T> Select<T, I>() where T : Entity<I>
{
return session.Query<T>();
}
public T SelectById<T>(int id) where T : Entity
{
return session.Query<T>().Single(x => x.Id == id);
}
public T SelectById<T, I>(I id) where T : Entity<I>
{
return session.Query<T>().Single(x => x.Id.Equals(id));
}
public void Update(object obj)
{
session.Update(obj);
}
}
}
using System.Data;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
namespace AgnosticsTest.Persistence
{
using AgnosticsTest.Domain.Persistence;
using AgnosticsTest.Persistence.Mapping;
using AgnosticsTest.Persistence.Mapping.Conventions;
public class UnitOfWorkFactory : IUnitOfWorkFactory
{
private ISessionFactory sessionFactory;
public UnitOfWorkFactory()
{
sessionFactory = Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(c => c.FromAppSetting("ConnectionString")))
.Mappings(m =>
{
m.FluentMappings.Add<AuthorMap>();
m.FluentMappings.Add<BoolMap>();
m.FluentMappings.Conventions.Add<HasManyConvention>();
m.FluentMappings.Conventions.Add<PrimaryKeyConvention>();
m.FluentMappings.Conventions.Add<ReferenceConvention>();
m.FluentMappings.Conventions.Add<TableNameConvention>();
})
.BuildSessionFactory();
 
}
public IUnitOfWork Create(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
{
return new UnitOfWork(sessionFactory, isolationLevel);
 
}
 
}
}

In this sample app, I merely had the UnitOfWorkFactory create the ISessionFactory at instantiation, but you could alter this and make it a static property, instantiating only if null. Also note that I probably should have had a Start() method in the UnitOfWork, and specifiy isolation level there. That way select only operations won’t be transactionalized. But this is a sample, so I pressed on.

I then imported my WindsorContainerResolver and extension methods to the Web project and added the following to Global.asax:

IWindsorContainer container = new WindsorContainer();
container.RegisterControllers(typeof(MvcApplication).Assembly);
container.Register(Component.For<IUnitOfWorkFactory>().ImplementedBy<UnitOfWorkFactory>());
 
DependencyResolver.SetResolver(new WindsorDependencyResolver(container));

That was basically it, except for adding a connectionstring to the web.config. Now, I just inject the unit of work factory into any services or controllers and use it like this:

[HttpPost]
public ActionResult Edit(BookEditModel model)
{
Book book;
using (var worker = unitOfWorkFactory.Create())
{
book = worker.SelectById<Book>(model.Id);
book.Title = model.Title;
worker.Update(book);
}
return RedirectToAction("Index");
}

Now we have a solution where both the UI and the Domain are 100% persistence ignorant. I can change from NHibernate to something else entirely by editing, or swapping out, the Persistence layer. Nothing at all need change in the web solution.

To be effective though, a few things need to change.

Things that need altering are:

  1. Move transaction initialization into a Start() method so that it is optional.
  2. Add session factory caching options to the factory.
  3. Tweak the windsor container registration for the factory for even better caching.

But basically, that’s it. I took the one-repository-per-aggregate-chain idea one more step and wrapper the whole of the domain. Why not I say, that’s how EF contexts do it, right? I also took convention over configuration another step and made a convention out of the connection string name. The rest is entirely persistence oriented anyway, so I saw no reason not to put it where it should go. I hope you found this, at the very least, insightful.