Tuesday, 20 September 2011

NHibernate session management and MVC

NHibernate session management is one of these things that occasionally catches me out. Usually once it's set up on a project I forget about it because it just works, but recently I came across a problem on a project which prompted me to write this post. There's a lot to say about NHibernate session management and this post is just a small contribution to the plethora of articles on the subject.

What follows here is my implementation of session management in an MVC web application (it shouldn't matter what version of MVC you're on). What I've set out to accomplish is this:
  • Define a unit of work by using NHibernate's ICurrentSessionContext
  • Enforce the unit of work by applying transaction management across controller calls.
  • Ensure that a session and transaction is available across multiple calls to child actions.
  • Make the session available for injection via my container of choice, Windsor.
In order to make any of this happen my application needs a session factory. I'm not going to go into the details of how I construct my session factory as it isn't really relevant to this post - but suffice to say that the creation of the session factory is triggered by the Application_Start() method in Global.asax - and that I store the session factory as a static property on the HttpApplication. Actually, there is one aspect of the session factory which is relevant to this post - but I'll come back to that a little later.

The Unit of Work
In web applications the obvious scope for a unit of work is a web request. Everything that happens, persistence-wise, between when a new request arrives and when the same request completes, can be treated as a unit of work. In other words, the changes to state in your application that takes place within a single web request should be atomic; the should either be persisted or rolled back at the end of the web request.

With this in mind I've set up the following methods on Global.asax:

private void OnBeginRequest(object sender, EventArgs args)
{
if(RequestMayRequirePersistence(sender as HttpApplication) && Factory != null)
{
var session = Factory.OpenSession();
session.FlushMode = FlushMode.Auto;
ManagedWebSessionContext.Bind(HttpContext.Current, session);
}
}

private void OnEndRequest(object sender, EventArgs args)
{
if (RequestMayRequirePersistence(sender as HttpApplication) && Factory != null)
{
ManagedWebSessionContext.Unbind(HttpContext.Current, Factory);
}
}
Ignore the call to RequestMayRequirePersistence - this is just a helper method to check whether the request is for a resource that may require an NHibernate session, such as a controller.

When a request arrives, I call OpenSession() on the session factory to obtain a new session. Then I bind this session to the current HttpContext by calling the Bind method on NHibernate's ManagedWebSessionContext.

Let's take a step back. Remember that I said that there's one aspect of the session factory set-up that's relevant? Well, we've already come to it. In order for this solution to work we need to tell the session factory which type of session context to use. A session context defines the scope of given session instance. In other words, if a session instance belongs to a context then the NHibernate session factory will keep serving up that same instance whenever a call is made to OpenSession() within that context. We can tell NHibernate which session context to use when setting up the session factory. Using FluentNHibernate syntax you can set up NHibernate to use the ManagedWebSessionContext by adding this to your session factory fluent config:
   .ExposeConfiguration(config =>
config.SetProperty(Environment.CurrentSessionContextClass,
typeof(ManagedWebSessionContext).AssemblyQualifiedName))
Now let's go back to handlers for BeginRequest and EndRequest. In addition to telling NHibernate which session context we want to use, we have to define when to bind and unbind the current session from that context. That's what the OnBeginRequest and OnEndRequest methods do; they define the actual scope of our NHibernate session, our unit of work.

You can read more about NHibernate session context here.

Enforcing our Unit of Work
Now we've defined a unit of work for our application. However, this unit of work is no good to us yet because we still need to set up some controls around when transactions are created, committed, and rolled back. For my application I think the following rules make sense:
  • Begin a transaction when an action method starts executing
  • If an exception occurs during the execution of an action, roll back the transaction.
  • If an exception occurs during the rendering of a view, roll back the transaction
  • If the execution of a controller and rendering of a view complete successfully, commit the transaction
  • Any calls to child actions should 'enlist' in the current transaction, and a child action should never roll back or commit a transaction.
I have implemented these rules through an ActionFilterAttribute which I've called, uhm, TransactionAttribute. It looks like this:
public class TransactionAttribute : ActionFilterAttribute
{
//Create transaction
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.Controller as IPersistenceController;

if (controller != null && !filterContext.IsChildAction)
{
controller.CurrentSession.BeginTransaction();
}
}

//rollback on exception
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var controller = filterContext.Controller as IPersistenceController;

if (controller != null && !filterContext.IsChildAction)
{
ITransaction transaction = controller.CurrentSession.Transaction;
if (transaction.IsActive)
{
if (filterContext.Exception != null)
{
transaction.Rollback();
}
}
}
}

//commit transaction, or rollback on exception
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
var controller = filterContext.Controller as IPersistenceController;
if (controller != null && !filterContext.IsChildAction)
{
ITransaction transaction = controller.CurrentSession.Transaction;
base.OnResultExecuted(filterContext);
try
{
if (transaction.IsActive)
{
if (filterContext.Exception != null)
transaction.Rollback();
else
transaction.Commit();
}
}
finally
{
transaction.Dispose();
}
}
base.OnResultExecuted(filterContext);
}
}
There are really only two things worth noting here:
  • Controllers are cast to IPersistenceController. IPersistenceController defines a single property ISession CurrentSession {get;set;} which is used to obtain the current session.
  • I check the IsChildAction property of the ActionExecutingContext to make sure that child actions don't mess with the current transaction.
You could argue that having every controller implement IPersistenceController is a bad thing and that letting the controllers know about ISession is even worse - and I'd probably tend to agree with you. If you don't like this approach you can of course abstract the access to the current session away from the controller. I've opted for this approach because of ease of implementation; Windsor - which I'll talk about in a minute - resolves all controllers for me, and therefore sets the ISession on the IPersistenceController.CurrentSession property automatically.

Making the Session Available to Windsor
The last piece of the puzzle is to make ISession available to Windsor so that it can be injected:
   container.Register(Component.For()
.UsingFactoryMethod(() => MvcApplication.Factory.GetCurrentSession())
.LifeStyle.PerWebRequest);
Note the use of a factory method, and the call to MvcApplication.Factory.GetCurrentSession(). When Windsor needs to resolve an instance of ISession it will call GetCurrentSession() on the application's session factory. The current session is governed by the session context which we set up earlier. See? So Windsor will be handed the same instance of ISession every time it asks the factory for it during the course of a single web request.

The last thing to notice is the lifestyle that's chosen for the ISession instance: PerWebRequest. If you choose anything other than this lifestyle you'll run into trouble:
  • If you set the lifestyle to Transient then Windsor will dispose of ISession if it disposes of any objects that have been injected with ISession
  • If you set the lifestyle to Singleton then Windsor will break the scope of the Unit of Work we've so carefully set up and you'll end up with a single session for the lifetime of the application.
Summary
In this post I've shown one way of managing NHibernate sessions in the context of an ASP.NET MVC application. There are many ways of doing this, but this way seems to work well for me. Comments are welcome!