Yet another session-per-request post

If you ever went on my old blog you may have seen a couple of posts I wrote discussing various session-per-request implementations. However, each of my previous implementations had flaws and I've finally got a solution that I'm happy with, that's working nicely in production.

In case you don't know what session-per-request is, the idea is simple, we create a session to our database per (http) request, typically beginning a transaction at the start of the request and committing a transaction at the end of the request, thus saving changes. It's often referred to as a unit of work although I would argue that not all changes that happen within a request are necessary part of the same "unit" of work.

Using an Action Filter

If I need to use a ORM, I'll typically use NHibernate. Previously I had an Action Filter that I used to handle the session:

public class UnitOfWorkAttribute : ActionFilterAttribute
{
    public Func<ISession> SessionFactory { get; set; }

    public override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext)
    {
        SessionFactory().BeginTransaction();
    }

    public override void OnResultExecuted(System.Web.Mvc.ResultExecutedContext filterContext)
    {
        var session = SessionFactory();

        var tx = session.Transaction;

        if (tx != null && tx.IsActive)
        {
            var thereWereNoExceptions = filterContext.Exception == null || filterContext.ExceptionHandled;
            if (filterContext.Controller.ViewData.ModelState.IsValid && thereWereNoExceptions)
                session.Transaction.Commit();
            else
                session.Transaction.Rollback();
        }
    }
}

I would use my DI tool (typically StructureMap) to inject the Func<ISession> and my ISession lifetime would be scoped per request. We then begin the transaction in OnActionExecuting and commit it in OnActionExecuted.

Note the reason for injecting a Func is that as of ASP.NET MVC 3, filters are cached in which case any instances you inject would also be cached - not so good if you're trying to have an instance "per request"

The above Action Filter worked fine on a number of projects. However I wasn't completely happy with it, for the following reasons.

  1. It meant I either had to reference NHibernate in my web application or System.Web.Mvc in my persistence layer, which felt a bit, meh.
  2. If I needed access to session before an action method was invoked then I was a bit stuck.

I'll expand on the second point. In a recent project I started making use of asp.net mvc model binders to clean up my controllers. It meant that instead of having code like this:

public ActionResult SavePage(Page page) {
    var site = siteRepo.GetById(siteContext.SiteId);

    page.Site = site;

    repo.Save(page);
    return RedirectToAction("index");
}

we could have a binder automatically wire up a site parameter:

public ActionResult SavePage(Site site, Page page) {
    page.Site = site;   
    repo.Save(page);
    return RedirectToAction("index");
}

(note this is really bad code for a number of reasons, I'm just proving a point).

Like I said, this helped cleaned up our controllers significantly and removed some constructor soup.

Since the model binders are executed before the action is invoked it meant the session would be used outside of a transaction.

Using a HttpModule

If we need to manage the session further up the request pipeline then we could use a HttpModule. This way we can begin our transaction at the very beginning of the request.

This works but again this method has some downfalls:

  1. The module is executed on every request. In the case of ISession, it is just a cheap object but I still don't like creating a session if we don't need it.
  2. We normally end up with static references to our IoC container because dependency injection into http modules is...complicated.

Using Awesomeness

So my new improved, never to be bettered (probably), session-per-request implementation is as follows.

First we start with a simple unit of work interface, which extracts the common steps of a transaction:

public interface IUnitOfWork : IDisposable
{
    void Begin();
    void Commit();
    void Rollback();
}

Now we can create a "UnitOfWorkFilter" that only needs a reference to the above (removing that NHibernate dependency):

public class UnitOfWorkAttribute : ActionFilterAttribute
{
    public Func<IUnitOfWork> UnitOfWorkFactory { get; set; }

    public override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext)
    {
        var uow = UnitOfWorkFactory();
        uow.Begin();
    }

    public override void OnResultExecuted(System.Web.Mvc.ResultExecutedContext filterContext)
    {
        var uow = UnitOfWorkFactory();

        try
        {
            if (filterContext.Exception == null || filterContext.ExceptionHandled)
            {
                uow.Commit();
            }
            else
            {
                uow.Rollback();
            }
        }
        catch
        {
            uow.Rollback();
            throw;
        }
        finally
        {
            uow.Dispose();
        }
    }
}

You might think at this point that we still have a problem if we need access to ISession before an action is invoked. Let's look at the NHibernate implementation:

    internal interface INHibernateUnitOfWork : IUnitOfWork
    {
            ISession Session { get; }
    }

    public class NHibernateUnitOfWork : INHibernateUnitOfWork
    {
            private readonly ISessionSource sessionSource;
            private ITransaction transaction;
            private ISession session;

            private bool disposed;
            private bool begun;

            public NHibernateUnitOfWork(ISessionSource sessionSource) {
                    this.sessionSource = sessionSource;
                    Begin();
            }

            private static readonly object @lock = new object(); 

            public ISession Session
            {
                    get
                    {
                            if (session == null)
                            {
                                    lock (@lock)
                                    {
                                            if (session == null)
                                            {
                                                    session = sessionSource.CreateSession();
                                            }
                                    }
                            }

                            return session;
                    }
            }

            public void Begin()
            {
                    CheckIsDisposed();

                    if (begun)
                    {
                            return;
                    }

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

                    transaction = Session.BeginTransaction(System.Data.IsolationLevel.ReadCommitted);

                    begun = true;
            }

            public void Commit()
            {
                    CheckIsDisposed();
                    CheckHasBegun();

                    if (transaction.IsActive && !transaction.WasRolledBack)
                    {
                            transaction.Commit();
                    }

                    begun = false;
            }

            public void Rollback()
            {
                    CheckIsDisposed();
                    CheckHasBegun();

                    if (transaction.IsActive)
                    {
                            transaction.Rollback();
                            Session.Clear();
                    }

                    begun = false;
            }

            public void Dispose()
            {
                    if (!begun || disposed)
                            return;

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

                    if (Session != null)
                    {
                            Session.Dispose();
                    }

                    disposed = true;
            }

            private void CheckHasBegun()
            {
                    if (!begun)
                    {
                            throw new InvalidOperationException("Must call Begin() on the unit of work before committing");
                    }
            }

            private void CheckIsDisposed()
            {
                    if (disposed)
                    {
                            throw new ObjectDisposedException(GetType().Name);
                    }
            }
    }

What's special about this implementation is that it will begin a transaction as soon as the unit of work is created. Further more, it allows the same session to be reused multiple times, handy if you are using session-per-thread and want to execute lots of individual transactions (you can just keep calling begin > commit > begin..).

The NHibernateUnitOfWork class has a dependency on ISessionSource. Implementations are responsible for building the NHibernate SessionFactory and creating sessions.

public interface ISessionSource
{
    ISession CreateSession();
    void BuildSchema();
}

public class NHibernateSessionSource : ISessionSource
{
    private const string ConnectionStringName = "MyConnectionStringName";
    private static readonly object factorySyncRoot = new object();

    private readonly IAppSettings appSettings;
    private readonly ISessionFactory sessionFactory;
    private readonly Configuration configuration;

    public NHibernateSessionSource(IAppSettings appSettings)
    {
        if (sessionFactory != null) return;

        lock (factorySyncRoot)
        {
            if (sessionFactory != null) return;

            this.appSettings = appSettings;
            configuration = AssembleConfiguration();
            sessionFactory = configuration.BuildSessionFactory();
        }
    }

    public Configuration AssembleConfiguration()
    {
        var connectionString = appSettings.GetSetting(ConnectionStringName);

        return Fluently.Configure()
            .Database(MsSqlConfiguration.MsSql2008
                .ConnectionString(connectionString)
                .AdoNetBatchSize(10)
            )
            .Mappings(cfg => {
                cfg.FluentMappings.AddFromAssemblyOf<NHibernateSessionSource>();
            })
            .BuildConfiguration();
    }

    public ISession CreateSession()
    {
        return sessionFactory.OpenSession();
    }
}

Finally the StructureMap registry that wires it all together:

    public class NHibernateRegistry : Registry
    {
            public NHibernateRegistry()
            {
                    For<ISessionSource>().Singleton().Use<NHibernateSessionSource>();

                    For<ISession>().Use(ctx => {
                            var uow = (INHibernateUnitOfWork)ctx.GetInstance<IUnitOfWork>();
                            return uow.Session;
                    });             

                    For<IUnitOfWork>().HybridHttpOrThreadLocalScoped().Use<NHibernateUnitOfWork>();
            }
    }

You can see that we are wiring up the ISession dependency to come from an instance of IUnitOfWork. This ensures that a) the transaction is always created regardless of when the ISession is requested and b) the session is shared for the entire request.

The missing piece is to decorate your controllers with the UnitOfWorkFilter or add it to the global filters collection.

Update

Please note that this example uses ASP.NET MVC 3. In order to inject dependencies into action filters, your DI tool will need to support property injection and you'll need an implementation of FilterAttributeFilterProvider. You can get one for StructureMap here.


Ben Foster

About Me

I'm a software engineer and aspiring entrepreneur with 12+ years experience in the tech industry and have worked with startups and SMB’s in areas such as healthcare, recruitment and e-commerce (I even worked in enterprise, once). I founded my first startup Fabrik in 2011.

I now head up the engineering team at Checkout.com. If you're interested in working in an exciting fin-tech company, drop me a message on Twitter.

Creative Commons Licence