I'm getting a pretty bizarre error using NHibernate. When I make a call to open session, I'm getting this error and stack trace.
The operation is not valid for the state of the transaction.-at System.Transactions.TransactionState.EnlistVolatile(InternalTransaction tx, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions, Transaction atomicTransaction)
at System.Transactions.Transaction.EnlistVolatile(IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions)
at NHibernate.Transaction.AdoNetWithDistributedTransactionFactory.EnlistInDistributedTransactionIfNeeded(ISessionImplementor session)
at NHibernate.Impl.AbstractSessionImpl.CheckAndUpdateSessionStatus()
at NHibernate.Impl.SessionImpl..ctor(IDbConnection connection, SessionFactoryImpl factory, Boolean autoclose, Int64 timestamp, IInterceptor interceptor, EntityMode entityMode, Boolean flushBeforeCompletionEnabled, Boolean autoCloseSessionEnabled, ConnectionReleaseMode connectionReleaseMode)
at NHibernate.Impl.SessionFactoryImpl.OpenSession(IDbConnection connection, Boolean autoClose, Int64 timestamp, IInterceptor sessionLocalInterceptor)
at NHibernate.Impl.SessionFactoryImpl.OpenSession(IInterceptor sessionLocalInterceptor)
This is only happening at one place in my application, and even then not consistently. In specific, this is code running inside a SharePoint application. SharePoint fires off my code whenever it receives email at a specific address. I only bring this up because to note that as a result, that each time the code is called its running on a separate thread, and there are no existing NHibernate transactions or sessions on that thread.
I cracked open the NHibernate source code and looked at the method throwing the error. As noted in the stack trace, it's the "EnlistInDistributedTransactionIfNeeded" method. This is the code for that method
if (session.TransactionContext != null)
return;
if (System.Transactions.Transaction.Current == null)
return;
var transactionContext = new DistributedTransactionContext(session,
System.Transactions.Transaction.Current);
session.TransactionContext = transactionContext;
logger.DebugFormat("enlisted into DTC transaction: {0}",
transactionContext.AmbientTransation.IsolationLevel);
session.AfterTransactionBegin(null);
transactionContext.AmbientTransation.TransactionCompleted +=
delegate(object sender, TransactionEventArgs e)
{
using (new SessionIdLoggingContext(session.SessionId))
{
((DistributedTransactionContext)session.TransactionContext).IsInActiveTransaction = false;
bool wasSuccessful = false;
try
{
wasSuccessful = e.Transaction.TransactionInformation.Status
== TransactionStatus.Committed;
}
catch (ObjectDisposedException ode)
{
logger.Warn("Completed transaction was disposed, assuming transaction rollback", ode);
}
session.AfterTransactionCompletion(wasSuccessful, null);
if (transactionContext.ShouldCloseSessionOnDistributedTransactionCompleted)
{
session.CloseSessionFromDistributedTransaction();
}
session.TransactionContext = null;
}
};
transactionContext.AmbientTransation.EnlistVolatile(transactionContext,
EnlistmentOptions.EnlistDuringPrepareRequired);
As you can see this method only really does anything if System.Transactions.Transaction.Current is not null. In my case, I can't understand any reason why it wouldn't be null, since the method trying to open the session doesn't open any other sessions or transactions, but I am no expert on distributed transactions.
A few other details that might be relevant
- My session factory is managed by a static object that exists for the lifetime of the webapplication/service.
- The sharepoint process that invokes my method is a windows service called OWSTimer. From what I understand (but haven't confirmed) it generates a separate thread for each incoming email, and then invokes my code on that thread.
- I have no need for distributed transactions, so if I can force NHibernate never to enlist my sessions in a distributed transaction, that's fine too.
- I have a number of event listeners that do interact with the session. In all cases I'm getting a session instance in the event listener with a call to var session = @event.Session.GetSession(EntityMode.Poco); Per the documentation I do not close these instances of the session. However, in a few cases I do call flush, simply because I missed that part of the documentation.
UPDATE:
Here's the static method that handles creating a session and is called by my other code.
public static ISession CreateAuditableSession(string siteUrl, ISharePointDataContext context)
{
var factory = Instance(siteUrl);
var session = factory.OpenSession();
var imp = session.GetSessionImplementation();
imp.Listeners.PreUpdateEventListeners = new IPreUpdateEventListener[] { new AuditUpdateListener(context) };
imp.Listeners.PostInsertEventListeners = new IPostInsertEventListener[] { new AuditUpdateListener(context) };
imp.Listeners.FlushEventListeners = new IFlushEventListener[] { new FixedDefaultFlushEventListener() };
imp.Listeners.PreInsertEventListeners = new IPreInsertEventListener[] { new PGUserDisplayNameRetrieverListener(context) };
imp.Listeners.PreDeleteEventListeners = new IPreDeleteEventListener[] { new AuditUpdateListener(context) };
imp.Listeners.PostCollectionUpdateEventListeners = new IPostCollectionUpdateEventListener[] { new AuditUpdateListener(context), new SupplierChangeEventListener() };
imp.Listeners.PostCollectionRecreateEventListeners = new IPostCollectionRecreateEventListener[] { new AuditUpdateListener(context), new SupplierChangeEventListener() };
imp.Listeners.PostLoadEventListeners = new IPostLoadEventListener[] { new PostLoadSubscriptionAndInjectionEventListener(context) };
return session;
}