views:

1156

answers:

0

My development group is about to release a version of an internal framework that uses NHibernate (and Spring.NET) for persistence. Developers currently using our internal framework use ADO.NET for persistence, so going forward they will use a combination of ADO.NET and NHibernate. During testing of the NHibernate, we found out that the NHibernate first level cache wasn't working as expected when we define a transaction with System.Transactions.TransactionScope.

Goal: 1) Get NHibernate first level cache working within TransactionScope 'using' block.

- Use TransactionScope using block to manage all transactions.
- Somehow need to manage NHibernate ITransaction along with TransactionScope ambient transaction (System.Transactions.Transaction.Current).

Assumptions: 1) Developers can use one or both of two repository implementations:

A) An application will access multiple databases using ADO.NET.
B) An application will use NHibernate to only access one database (one session factory).
C) Tables mapped to NHibernate may reside in the same database as tables accessed by ADO.NET.

2) Existing application code uses TransactionScope 'using' block to handle distributed transactions; this will not change. 3) We are unable to use Spring [Transaction] attribute or TransactionTemplate to manage transactions at this time because we cannot force developers to modify existing code (current directive).

Problem: In order to get NHibernate first level cache to work, we need to get the connection from the NHibernate session. And, to get the connection from the NHibernate session, we must be within an NHibernate transaction because we have specified the NHibernateTransactionManager as our Spring.NET IPlatformTransactionManager. So, we need to find a way to kick off a transaction without using [Transaction] or TransactionTemplate.

We are using the following to get the connection from the NHibernate session (A and B are the same behind the scenes?): A) ConnectionUtils.GetConnection(dbProvider) B) SessionFactory.GetCurrentSession().Connection

In order to kick off an NHibernate transaction when developers call 'using (new TransactionScope())' we intercept the creation of TransactionScope as follows:

Code:

using (TransactionScope transaction = DatabaseHelper.GetTransactionScope())
{
     ...
}

DatabaseHelper is a public, static class that developers use for database access helper methods. DatabaseHelper.GetTransactionScope() gets the TransactionScope object from the following factory class:

NHibernateTransactionScopeFactory : TransactionScopeFactory (abstract class derived from ITransactionScopeFactory)

Code:

/// <summary>
/// An interface for obtaining a <see cref="TransactionScope"/>.
/// </summary>
public interface ITransactionScopeFactory
{
    /// <summary>
    /// Get a transaction scope.
    /// </summary>
    /// <param name="transactionOptions">The options to use when creating the <see cref="TransactionScope"/> object.</param>
    /// <returns>A <see cref="TransactionScope"/> object.</returns>
    TransactionScope CreateTransactionScope(TransactionOptions transactionOptions);

    /// <summary>
    /// Transaction management object.  Used to help Spring.NET manage transactions
    /// between NHibernate's ITransaction and <see cref="TransactionScope"/>.
    /// </summary>
    IPlatformTransactionManager TransactionManager { get; set; }
}

The NHibernateTransactionScopeFactory is injected with the defined IPlatformTransactionManager. The NHibernateTransactionScopeFactory calls TransactionManager.GetTransaction() in the NHibernateTransactionScopeFactory.CreateTransactionScope() method in order to kick off the NHibernate transaction and bind the NHibernate session to thread local storage. The NHibernateTransactionScopeFactory stores the ITransactionStatus object returned from the TransactionManager.GetTransaction() method in thread local storage. The NHibernateTransactionScopeFactory subscribes to the Transaction.Current.TransactionCompleted event in order to call TransactionManager.Commit() or TransactionManager.Rollback() using the ITransactionStatus object that was returned from the GetTransaction() method.

Code for NHibernateTransactionScopeFactory.CreateTransactionScope() method:

bool newTransaction = null == Transaction.Current;

if (newTransaction)
{
    longSession = SessionFactoryUtils.GetNewSession(sessionFactory, null);
    longSession.FlushMode = FlushMode.Auto;

    IPlatformTransactionManager transactionManager = (IPlatformTransactionManager)SpringHelper.GetObjec t("transactionManager");
    ITransactionDefinition def = new DefaultTransactionDefinition();
    transactionStatus = transactionManager.GetTransaction(new DefaultTransactionDefinition());
}

TransactionScope transactionScope = new TransactionScope(TransactionScopeOption.Required, transactionOptions);

if (newTransaction)
{
    Transaction.Current.TransactionCompleted += new TransactionCompletedEventHandler(Current_Transacti onCompleted);
}

The Current_TransactionCompleted method checks the status of the Transaction and commits or rollsback the NHibernate transaction appropriately.

Another thing we had to do was, for all subsequent Database operations within the scope of the outer TransactionScope, get the connection from the session and enlist all the commands in the NHibernate transaction.

We are running into an issue when we have events though. Here is the scenario:

Our test has a Parent object with two Child objects. Both the Parent and Child object have OnSave events that set the auditing information (Created and Modified User and date information) in the objects.

Here is the sequence of events:

  • Start NHibernate session and Transaction
  • Start TransactionScope
  • Call Parent.Save
  • Parent.OnSave event fires. This starts a TransactionScope, which enlists in the ambient NHibernate transaction.
  • NHibernate cascades the save to Child.Save
  • Child.Save event fires. This starts a TransactionScope, which enlists in the ambient NHibernate transaction.
  • Parent and child objects are saved.

  • TransactionScope.Completed even fires

  • Commit the NHibernate transaction
  • This causes a session.Flush()
  • This cascades the save operation for the Child collection.
  • Child.Save event fires. This starts a TransactionScope. By this time, the NHibernate transaction is still active, but out Transaction.Current is null. So, when this TransactionScope completes, it tries to commit the NHibernate transaction again, which results in the following exception:

Spring.Transaction.IllegalTransactionStateExceptio n: Transaction is already completed - do not call commit or rollback more than once per transaction.

For testing purposes, we tried to commit the NHibernate transaction explicitly from the test instead of firing it from the TransactionScope.Completed event and things work fine. So, this leaves us with two issues:

  • Can we trap a 'BeforeCompleted' event for System.Transaction?
  • Can we stop NHibernate from firing the Save for the collection during the Commit?

Are there any other things we could try. Also, I would like to know if this approach is valid and see if we are overlooking any other issues.

Any help is appreciated.