1
votes

I am trying to use NHibernate to save to a database in the same transaction as sending a message on the bus from inside an MVC application:

public void DoSomethingToEntity(Guid id)
{
    var session = _sessionFactory.OpenSession();
    CurrentSessionContext.Bind(session);

    using (var transactionScope = new TransactionScope())
    {
        var myEntity = _session.Get(id);
        myEntity.DoSomething();
        _session.Save(myEntity);
        _bus.Send(myMessage);
        transactionScope.Complete();
    }

    session.Dispose();
}

In the configuration, .MsmqTransport() is set with .IsTransactional(true).

If I do this inside a message handler (which is wrapped in its own transaction so does not need the TransactionScope) Then it all works as expected, and if I include an exception, both fail.

However, if I do it inside my own transaction in an MVC application, I get the following error after transactionScope.Complete() when leaving the using block.:

'The operation is not valid for the current state of the enlistment.'

Stack Trace: at System.Transactions.EnlistmentState.InternalIndoubt(InternalEnlistment enlistment) at System.Transactions.VolatileDemultiplexer.BroadcastInDoubt(VolatileEnlistmentSet& volatiles) at System.Transactions.TransactionStatePromotedIndoubt.EnterState(InternalTransaction tx) at System.Transactions.TransactionStatePromotedBase.InDoubtFromEnlistment(InternalTransaction tx) at System.Transactions.DurableEnlistmentDelegated.InDoubt(InternalEnlistment enlistment, Exception e) at System.Transactions.SinglePhaseEnlistment.InDoubt(Exception e) at System.Data.SqlClient.SqlDelegatedTransaction.SinglePhaseCommit(SinglePhaseEnlistment enlistment) at System.Transactions.TransactionStateDelegatedCommitting.EnterState(InternalTransaction tx) at System.Transactions.TransactionStateDelegated.BeginCommit(InternalTransaction tx, Boolean asyncCommit, AsyncCallback asyncCallback, Object asyncState) at System.Transactions.CommittableTransaction.Commit() at System.Transactions.TransactionScope.InternalDispose() at System.Transactions.TransactionScope.Dispose() at HumanResources.Application.Implementations.HolidayService.Book(BookHolidayRequest request) in C:\Users\paul.davies\Documents\GitHub\EdaCalendarExample\HumanResources.Application\Implementations\HolidayService.cs:line 76 at HumanResources.UI.Controllers.HolidayController.BookUpdate(BookHolidayViewModel viewModel) in C:\Users\paul.davies\Documents\GitHub\EdaCalendarExample\HumanResources.UI\Controllers\HolidayController.cs:line 82 at lambda_method(Closure , ControllerBase , Object[] ) at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) at System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary2 parameters) at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary2 parameters) at System.Web.Mvc.ControllerActionInvoker.<>c_DisplayClass15.b_12() at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation)

Latest Edit:

This code works:

public void DoSomethingToEntity(Guid id)
{
    var session = _sessionFactory.OpenSession();
    CurrentSessionContext.Bind(session);

    using (var transactionScope = new TransactionScope())
    {
        var myEntity = _session.Get(id);
        _bus.Send(myMessage);
        transactionScope.Complete();
    }

    session.Dispose();
}

This code creates the error:

public void DoSomethingToEntity(Guid id)
{
    var session = _sessionFactory.OpenSession();
    CurrentSessionContext.Bind(session);

    using (var transactionScope = new TransactionScope())
    {
        var myEntity = _session.Get(id);
        myEntity.AnyField = "a new value";
        _bus.Send(myMessage);
        transactionScope.Complete();
    }

    session.Dispose();
}

Note that I am not saving th entity in either example. The difference is in the second example, I am modifying the entity I have got from NHibernate. This is 100% reproducable.

2
Would it be possible to send the message first from the web app and then have NSB do the DB update and any other subsequent Sends()? - Adam Fyles
@AdamFyles This would be possible and would work but incurs a performance overhead which I'm sure could be avoided. I know using TransactionScope is possible but I can't get past this error. - Paul T Davies
To whoever downvoted: it is not helpful to me or other Stack Overflow users to downvote without an explanation why. - Paul T Davies
It seems like the EX happens when the sql driver tried to upgrade the TX to a distributed transaction (since the msmq send will force the upgrade). The reason that it works when running on a handler is that NSB has already received from msmq so the TX is already a distributed TX when the sql driver enlists. No idea why it happens though - Andreas Öhlund
This may not be related but you still have to call _session.Flush() before committing a TransactionScope even if the session flush mode is set to Commit - that only works for NH provided transactions. - eulerfx

2 Answers

4
votes

This may not be related but you still have to call _session.Flush() before committing a TransactionScope even if the session flush mode is set to Commit - that only works for NH provided transactions.

0
votes

As far as I can tell there is no way of being notified when a new System.Transactions.Transaction is created, and looking at the code in NHibernate it doesn't seem to have any code to deal with the situation where the TransactionScope is created AFTER creating the session.

When you create the session, it will try to enlist in the current Transaction, and if there isn't one then the session won't enlist in the transaction. I suspect that this is what's causing the transaction to fail on commit.

I would suggest creating the session INSIDE the TransactionScope - also check whether you are calling session.BeginTransaction somewhere before the TransactionScope.