nHibernate, session, webapi - how to commit / rollback

May 28, 2014 at 1:17 PM
Hi,

I am slowly getting the hang of using SI. Nice piece of work.

I am using it in WebAPI to inject nhibernate session into business manager classes without injecting it into the WebApi controller. The pseudo code looks like:
////////////////////////////////////
// UsersController.cs

public class UsersController : System.Web.Http.ApiController
{
   IUserManager _userManager;

   // IUserManager injected by SI
   public OrgUsersController(IUserManager userManager)
   {
      _userManager = userManager;
   }

   [HttpGet]
   [Route("api/users")]
   public HttpResponseMessage GetUsers()
   {
      IList<User> users = _userManager.GetUsers();
      return Request.CreateResponse(HttpStatusCode.OK, users);
   }
}
////////////////////////////////////

////////////////////////////////////
// UserManagerImpl.cs

public class UserManagerImpl : IUserManager  {
   ISession _session;

   // ISession injected by SI
   public UserManagerImpl (ISession session)
   {
      _session = session;
   }

   public IList<User> GetUsers() {
      // fetch users from database and return the list
   }
}
Now the problem I am facing is, how to commit the data. I don't want to do it in the method of the manager class, because the particular action may involve interacting with multiple manager classes, and commit should happen only if all of them succeed.

I am right now doing this in the action and exception filters of WebAPI (by injecting session into the controller), but that means I need to expose the use of nHibernate to my controller and action filter classes. Is there a way to avoid this?

Is there a way that I can tell SI to call commit on success, and rollback in case there is an exception? I tried using scopedLifeStyle.WhenScopeEnds, but that gets called even when there
is an exception.

Any suggestions?

Thanks,
Vikram
Coordinator
May 28, 2014 at 1:38 PM
Edited May 28, 2014 at 6:39 PM
>> Is there a way that I can tell SI to call commit on success, and rollback in case there is an exception?

There is no way that Simple Injector (or any tool for that matter) to know whether it is safe to commit or not. Deciding whether to commit or not based on whether the code is running inside the context of an exception based on some contextual information is dangerous and unreliable and you should prevent from doing that.

Please take a look at this Stackoverflow Q/A for a more detailed discussion on having per-request unit of works.

Basically, the advice of that answer is to wrap the service with a decorator that allows committing the operation in case no exception was thrown. Downside of your approach however is that you probably have many non-generic service abstractions (besides your IUserManager, you probably have abstractions like ICustomerManager, IProductManager, IOrderManager). With such design you'll end up with a decorator per manager, and this will cause you to have lots of duplicate code (you'll violate DRY). So instead, the Stackoverflow answer advises to apply the command/handler pattern, because that allows you to create one generic decorator that can be applied around every command handler.
May 29, 2014 at 5:22 AM
Thanks DNJ.

I get your point.

As I mentioned, I had initially thought of implementing the commit / rollback through action and exception filters decorating the WebApi controller (s). That approach was leaky in the sense it was forcing me to expose ISession to controller. Controller itself does not have any use of ISession (therefore it should not know about it). This has a benefit that I don't need a separate decorator for each manager.

Based on your comment, I am wondering if I should have my own wrapper on ISession to abstract it out from the Controller.
public interface IActionTransaction 
{
   void Success();

   void Fail();
}
I can inject an ISession aware implementation of IActionTransaction into controller, and commit /rollback in the action / exception filters supported by WebApi Controller. It will work, but the solution feels a bit contrived.

Vikram
Coordinator
May 29, 2014 at 8:41 AM
You can't do transaction handling in your exception filters, because this is simply the wrong layer. Take a look at the following example that demonstrates the problem:
public class OrderManagerImpl : IOrderManager  {
   private ISession session;
   private IShippingDepartment shippingDepartment;
   // ctor omitted
   public void ShipOrder(Guid orderId) {
      var order = session.Orders.GetById(orderId);

      order.State = OrderState.ReadyForShipping;

      // This method might throw a ValidationException
      this.shippingDepartment.AddOrderToShippingQueue(order);   
   }
}

public class OrderController : Controller
{
    private readonly IOrderManager orderManager;
    // ctor omitted

    [HttpPost]
    public ActionResult Ship(Guid orderId) {
        try {
            this.orderManager.ShipIorder(orderId);
        } catch (ValidationException ex) {
            // AddModelErrors is a convenient custom extension method.
            this.ModelState.AddModelErrors(ex);
        }
    }
}
The code above looks pretty innocent: The OrderManagerImpl contains a ShipOrder method that does the proper processing for the "ship order use case". It can throw ValidationExceptions that communicate validation errors and those errors can be caught at the presentation layer and presented to the user in a user friendly way.

But if you look closely at the implementation of the OrderManagerImpl, you see it changes the state of the Order before throwing a ValidationException. But you can also imagine the IShippingDepartment doing exactly the same. So when the exception is thrown, there's some uncommitted state and since an exception was thrown from the business layer we expect the operation not to be committed. The OrderController however catches that specific exception since it knows how to handle that specific exception. Since it handled this exception correctly, it doesn't rethrow that exception; that's obvious, since we won't want to confront the user with an ugly error page in case there is a validation error. But the result of this is that the exception filter will never know that the operation failed, and it will still commit the transaction, causing the order to be flagged as ReadyForShipping.

And as a matter of fact, this is just a simple example. Things can become very complex. Of course you can have a guideline that the presentation layer should never catch any exceptions that are originated from the business layer, but that can also introduce a lot of complexity. The real problem here is that the business layer itself isn't in control of the transaction. That transaction should have been either committed or rolled back BEFORE the control is passed back to the presentation layer.

So, the use of an IActionTransaction interface could work, as long as you make sure that it is used before the business operation returns back to the presentation layer. There are a few ways to do this:
  • You can implement this transaction logic inside your business logic methods (such as the UserManagerImpl.GetUsers method), but of course this gives a lot of duplicate code and makes your code harder to read and maintain.
  • You can create a base class for business logic that needs transactions and use the logic from the base class, but downside is that this still hard-couples the implementation to the cross-cutting concerns, making code harder to test and maintain, and limits the flexibility.
  • You can create decorators for your business logic that implement these cross-cutting concerns, but that does mean you need to change your design to prevent yourself from having many decorators with still lots of duplicate code.
I'm pretty used to applying the command/handler pattern to my applications, since this gives lots of great benefits, and apart from the mental shift, I haven't encountered any downsides to that approach.
Marked as answer by dot_NET_Junkie on 5/30/2014 at 2:20 AM
May 29, 2014 at 9:13 AM
Steven,

Thanks for your input. I understand the mistake of performing a rollback in exception filter. You are right - it is not the right layer to do the job.

I have never used command/handler pattern, so I guess time to buckle up, understand it and see how it can be applied in our application.

Once again, thanks for excellent advice.

Vikram