1

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.

Paul T Davies
  • 2,527
  • 2
  • 22
  • 39
  • 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 Sep 19 '12 at 13:48
  • @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 Sep 19 '12 at 15:31
  • To whoever downvoted: it is not helpful to me or other Stack Overflow users to downvote without an explanation why. – Paul T Davies Sep 19 '12 at 15:32
  • Is the DTC running on your webserver? – Andreas Öhlund Sep 19 '12 at 16:18
  • @PaulTDavies this would allow you to immediately return and let the DB work on its own time in the background which should have better performance. – Adam Fyles Sep 19 '12 at 17:59
  • @AndreasÖhlund DTC all seems to be up and running, and has the same settings as in this article: http://www.deepakkapoor.net/turn-on-msdtc-windows-7/ If DTC was the problem, wouldn't it also be a problem in the message handlers? – Paul T Davies Sep 20 '12 at 07:55
  • 1
    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 Sep 20 '12 at 20:29
  • Could this help? http://davybrion.com/blog/2010/03/msdtc-woes-with-nservicebus-and-nhibernate/ – Andreas Öhlund Sep 20 '12 at 20:32
  • 1
    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 Sep 21 '12 at 02:37
  • @AndreasÖhlund I feel I'm getting a bit closer. When I said it still fails with the NHibernate functionality removed, I was only talking about the save. If I remove anything to do with NH, it works. I'm getting closer to this and will update you. Please post an answer to collect SO points! – Paul T Davies Sep 21 '12 at 12:46
  • @eulerfx You got it! session.Flush() fixed it. Sorry to everyone if this is something I should not have missed. Stick an answer on to claim you points! – Paul T Davies Sep 21 '12 at 13:17
  • @AndreasÖhlund The above should interest you. Sorry if this is in the documentation somewhere and I missed it! – Paul T Davies Sep 21 '12 at 13:17

2 Answers2

4

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
  • 36,769
  • 7
  • 61
  • 83
  • Yes, calling session.Flush(); before transactionScope.Complete() fixed the problem. I don't fully understand why, but is to do with having both NServiceBus and NHibernate (or more specifically, ADO.NET, I think) inside the transaction. – Paul T Davies Sep 21 '12 at 14:59
0

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.

Martin Ernst
  • 5,629
  • 2
  • 17
  • 14