Monday, March 21, 2011

NHibernate unit of work in MVC using Ninject (part 2)

In part 1 I talked about a problematic NHibernate Unit of Work implementation that we had in our MVC application. In part 2 I will talk about why this approach turned out to be problematic and the ultimate solution that we came up with.

If you recall, the original solution was to use an HttpModule to subscribe to the BeginRequest and EndRequest events. When we upgraded to Ninject 2.1, none of our entities would get saved. Reads all worked fine, but no CUD actions worked. The problem turned out to be due to a new component of Ninject called the OnePerRequestModule. This module subscribes to the EndRequest event and disposes any objects that were bound in request scope. The problem was that this module would dispose of our session, before we had a chance to commit the transaction and end the session ourselves. It was also registering first (which meant it was first to handle EndRequest) and there appeared to be no hook to turn it off.

The solution that we came up with was to use a filter attribute in order to encapsulate our unit of work. The first part of the refactor was to create an IUnitOfWork interface:

public interface IUnitOfWork
{
 void Begin();
 void End();
}

That's all a unit of work really should look like. Second we moved our BeginSession and EndSession code into an implementation of IUnitOfWork:

public class NHibernateUnitOfWork : IUnitOfWork, IDisposable
{
 private readonly ISession _session;

 public NHibernateUnitOfWork(ISession session)
 {
  _session = session;
 }

 public void Begin()
 {
  _session.BeginTransaction();
 }

 public void End()
 {
  if (_session.IsOpen)
  {
   CommitTransaction();
   _session.Close();
  }
  _session.Dispose();
 }

 private void CommitTransaction()
 {
  if (_session.Transaction != null && _session.Transaction.IsActive)
  {
   try
   {
    _session.Transaction.Commit();
   }
   catch (Exception)
   {
    _session.Transaction.Rollback();
    throw;
   }
  }
 }

 public void Dispose()
 {
  End();
 }
}

This separated repository logic from the unit of work implementation. The binding for this implementation was bound in request scope. Finally we created our filter attribute:

public class UnitOfWorkAttribute : FilterAttribute, IActionFilter
{
 [Inject]
 public IUnitOfWork UnitOfWork { get; set; }

 public UnitOfWorkAttribute()
 {
  Order = 0;
 }

 public void OnActionExecuting(ActionExecutingContext filterContext)
 {
  UnitOfWork.Begin();
 }

 public void OnActionExecuted(ActionExecutedContext filterContext)
 {
  UnitOfWork.End();
 }
}

The order was set to 0 to ensure that this filter was always run first. We also did not have to explicitly call End on the unit of work since it would be disposed at the end of the request and would call End itself. However, it's good practice to be explicit with these things. Finally you can apply this filter at the controller level or the action level. So you can decorate only those actions that actually perform data access.
8USTXZWHRBTH

6 comments:

  1. Vadim: How are you injecting the UnitOfWork property on the attribute? I am using StructureMap (which I know you aren't), but I am finding problems in doing so.

    ReplyDelete
  2. @pghtech I'm using MVC2 and the Ninject.Web.Mvc project, for that version, has its own ControllerFactory that injects any dependencies into the filters. If you note there is a InjectAttribute decorating the UnitOfWork property. That tells Ninject that it needs to inject that property when creating the attribute.
    If you're using MVC3 here's a write up on how to use StructureMap to create a FilterProvider.

    ReplyDelete
  3. Very nice, this clears up some things regarding why my BeginRequest/EndRequest solution isn't working. Just curious, do you have a recommended way to deal with rolling back open transactions when an unhandled exception occurs elsewhere in the application?

    ReplyDelete
  4. @Todd, you can have your UnitOfWorkAttribute implement the IExceptionFilter. This will allow you to implement the OnException method where you can roll back your transaction.

    ReplyDelete
  5. Nice solution, but I do see a violation of separation of concerns. The presentation layer has to know details about database persistence; but I do think this could be a small sacrifice for what you are getting.

    ReplyDelete
    Replies
    1. The presentation only needs to know if whatever it is doing needs to be encapsulated in a unit of work. It's technology agnostic. You're right though, it's not 100% optimal.

      Delete