How to rollback nHibernate transaction when an exc

2019-05-26 05:39发布

问题:

I use nHibernate for ORM and Ninject for IoC. I create nHibernate sessions per some custom scope (which you can assume is per request). I begin the transaction onActivation. I commit the transaction onDeactivation.

The problem is that if an exception happens during the request I want to rollback the transaction rather than committing it. Any idea how to detect (in a clean way, most probably using Ninject Context) that an exception has happened?

Note: I am not concerned about the exceptions that can happen on commit which I can catch in the following code and role back easily.

protected void BindWithSessionWrapper<T>(Func<IContext, T> creationFunc) where T : ISessionWrapper
{
    Bind<T>().ToMethod(creationFunc)
        .InScope(x => new NinjectCustomScope()) // work in progress !!!
        .OnActivation(t => t.Session.BeginTransaction(IsolationLevel.ReadCommitted))
        .OnDeactivation((c, t) => 
            { 
                t.Session.Transaction.Commit();
                t.Session.Dispose();
            });
}

Update:

I followed the suggestion by @BatteryBackupUnit. So I added the following to the Error EventHandler:

    Error += (s, e) =>
        {
            HttpContext.Current.Items["ErrorRaised"] = true;
        };

And I modified the OnDeactivation to look like this:

OnDeactivation(t => 
                    { 
                        if ((bool?)HttpContext.Current.Items["ErrorRaised"] == true)
                            t.Session.Transaction.Rollback();
                        else
                            t.Session.Transaction.Commit();

                        t.Session.Dispose();
                    });

It works fine, but that would be better if Ninject would take care of this by setting a flag in the Context if an exception happened :)

回答1:

How about implementing an IHTTPModule and subscribing to the Error event? Like described here

In the Error event handler, use System.Web.Mvc.DependencyResolver.Current.GetService(typeof (ISession)) to retrieve the current session and rollback the transaction.

Note, however, that in case the request did not use a session, this will create one, which is quite superfluous.

You might do something like checking whether a transaction was started and only then rolling it back. But you'd still create a session unnecessarily.

You could further improve that by using the Error event handler to set a flag on HttpContext.Current.Items, like

HttpContext.Current.Items["RollbackTransaction"] = true;

and then use it in the OnDeactivation of the session like:

    .OnDeactivation((c, t) => 
        { 
            if(HttpContext.Current.Items.Contains("RollbackTransaction"])
            {
                t.Session.Transaction.Rollback();
            }
            else
            {
                t.Session.Transaction.Commit();
            }
            t.Session.Dispose();
        });

Please note that HttpContext is thread local, that means when you switch threads it may be null or -worst case - it might even be another HttpContext.

Please also note that i was unable to try it out so it may not work. Feedback appreciated.



回答2:

Passing the state through HttpContext is not acceptable to me for 2 reasons.

  1. HttpContext issue: https://stackoverflow.com/a/12219078/656430)
  2. Passing state seems like passing a global state (https://softwareengineering.stackexchange.com/questions/148108/why-is-global-state-so-evil)

After a lot of trial and error, I think this should be one solution: Assuming we are working on WebApi project, having rollback transaction for all actions once hit exception, with Ninject:

  1. install Ninject.Extension.Factory (https://www.nuget.org/packages/Ninject.Extensions.Factory/), this is very important step as to inject ISession in request scope into filters.
  2. use the following configuration for binding ISessionFactory and ISession (I made use of this example: Need a simple example of using nhibernate + unit of work + repository pattern + service layer + ninject), plus ISessionInRequestScopeFactory

    Bind<ISessionFactory>().ToProvider<NhibernateSessionFactoryProvider>().InSingletonScope();
    Bind<ISession>()
            .ToMethod(context => context.Kernel.Get<ISessionFactory>().OpenSession())
            .InRequestScope(); // notice that we don't need to call `BeginTransaction` at this moment 
    Bind<ISessionInRequestScopeFactory>().ToFactory(); // you don't need to make your implementation, the Ninject.Extension.Factory extension will help you so.
    
  3. the code for interface ISessionInRequestScopeFactory:

    public interface ISessionInRequestScopeFactory
    {
        ISession CreateSessionInRequestScope(); // return ISession in the request scope
    }
    
  4. Make use of ninject filter injection to add Transaction behaviour to every action (https://github.com/ninject/Ninject.Web.WebApi/wiki/Dependency-injection-for-filters):

    Kernel.BindHttpFilter<ApiTransactionFilter>(System.Web.Http.Filters.FilterScope.Action)
        .WhenControllerHas<ApiTransactionAttribute>();
    
  5. add [ApiTransaction] attribute into controller:

     [ApiTransaction]
     public class YourApiController{ /* ... */}
    
  6. So we are now binding the ApiTransactionFilter into YourApiController which are having [ApiTransaction] Attribute

  7. Inside ApiTransactionFilter, you should extends AbstractActionFilter and inject the factory ISessionInRequestScopeFactory for getting the correct request scope session:

    public class ApiTransactionFilter : AbstractActionFilter{
        private readonly ISessionInRequestScopeFactory factory;
    
        public ApiTransactionFilter(ISessionInRequestScopeFactory factory){
            this.factory = factory;
        }
    
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            ISession session = factory.CreateSessionInRequestScope(); // get the request scope session through factory
            session.BeginTransaction(); // session can begin transaction here ... 
            base.OnActionExecuting(actionContext);
        }
    
        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            ISession session = factory.CreateSessionInRequestScope(); // get the request scope session through factory
            if (actionExecutedContext.Exception == null) // NO EXCEPTION!
            {
                session.Transaction.Commit();// session commit here ... may be you like to have try catch here
            }
            else
            {
               session.Transaction.Rollback(); // session rollback here ...
            }
    
            base.OnActionExecuted(actionExecutedContext);
        }
    }