views:

786

answers:

2

I'm trying to find the best solution to handle transaction in a web application that uses NHibernate.

We use a IHttpModule and at HttpApplication.BeginRequest we open a new session and we bind it to the HttpContext with ManagedWebSessionContext.Bind(context, session); We close and unbind the session on HttpApplication.EndRequest.

In our Repository base class, we always wrapped a transaction around our SaveOrUpdate, Delete, Get methods like, according to best practice:

        public virtual void Save(T entity)
        {
          var session = DependencyManager.Resolve<ISession>();
          using (var transaction = session.BeginTransaction())
          {
            session.SaveOrUpdate(entity);
            transaction.Commit();
          }
        }

But then this doesn't work, if you need to put a transaction somewhere in e.g. a Application service to include several repository calls to Save, Delete, etc..

So what we tried is to use TransactionScope (I didn't want to write my own transactionmanager). To test that this worked, I use an outer TransactionScope that doesn't call .Complete() to force a rollback:

Repository Save():

    public virtual void Save(T entity)
    {
        using (TransactionScope scope = new TransactionScope())
        {
            var session = Depe.ndencyManager.Resolve<ISession>();
            session.SaveOrUpdate(entity);
            scope.Complete();
        }   
    }

The block that uses the repository:

        TestEntity testEntity = new TestEntity { Text = "Test1" };
        ITestRepository testRepository = DependencyManager.Resolve<ITestRepository>();

        testRepository.Save(testEntity);

        using (var scope = new TransactionScope())
        {
          TestEntity entityToChange = testRepository.GetById(testEntity.Id);

          entityToChange.Text = "TestChanged";
          testRepository.Save(entityToChange);
        }

        TestEntity entityChanged = testRepository.GetById(testEntity.Id);

        Assert.That(entityChanged.Text, Is.EqualTo("Test1"));

This doesn't work. But to me if NHibernate supports TransactionScope it would! What happens is that there is no ROLLBACK at all in the database but when the testRepository.GetById(testEntity.Id); statement is executed a UPDATE with SET Text = "TestCahgned" is fired instead (It should have been fired between BEGIN TRAN and ROLLBACK TRAN). NHibernate reads the value from the level1 cache and fires a UPDATE to the database. Not expected behaviour!? From what I understand whenever a rollback is done in the scope of NHibernate you also need to close and unbind the current session.

My question is: Does anyone know of a good way to do this using TransactionScope and ManagedWebSessionContext?

+1  A: 

I took a very similar approach. In the HttpModule I ask the sessionfactory for a new session + bind it when a new request comes in. But I also begin the transaction here. Then when the request is ending I simply unbind it and attempt to commit the transaction.

Also my base repository doesn't take a session in any way - it instead will ask for the current session and then perform some work with the session. Also I don't wrap anything inside this base class with a transaction. Instead the entire http request is a single unit of work.

This might not be appropriate for the project you are working on, but I prefer this approach because each request will fail or succeed as a single atomic unit. I have a full blog post here with source code if you are interested in the actual implementation.

The below is a sample of what this base repository looks like:

public abstract class NHibernateRepository<T> where T : class
{

    protected readonly ISessionBuilder mSessionBuilder;

    public NHibernateRepository()
    {
        mSessionBuilder = SessionBuilderFactory.CurrentBuilder;
    }

    public T Retrieve(int id)
    {
            ISession session = GetSession();

            return session.Get<T>(id);
    }

    public void Save(T entity)
    {
            ISession session = GetSession();

            session.SaveOrUpdate(entity);
    }

    public void Delete(T entity)
    {
            ISession session = GetSession();

            session.Delete(entity);
    }

    public IQueryable<T> RetrieveAll() 
    { 
            ISession session = GetSession();

            var query = from Item in session.Linq<T>() select Item; 

            return query; 
    }

    protected virtual ISession GetSession()
    {
        return mSessionBuilder.CurrentSession;
    }
}
Toran Billups
+1  A: 

Thanks for the answer!

Yes, it's a simple and straightforward way to solve it. But my problem is that I want to make sure that there is a transaction surrounding a repository operation, even if the application service, repository etc is not called by a web request (other types of clients), therefore I wanted to have a transaction surrounding the lowest level (e.g. session.Save) and then use TransactionScope to create a longer transaction if needed. But your solution is simple and I like that, mabye I'll use that and then make sure that other clients use transactions aswell.

Erik
so are you using this "service" in the context of WCF / ASMX or is this a domain like service inside your web application?
Toran Billups