tags:

views:

211

answers:

2

I've created an integration test to verify that a repository handles Concurrency correcly. If I run the test without a TransactionScope, everything works as expect, but if I wrap the test in a TransactionScope, I get an error suggesting that there is a sudden need for distributed transactions (which lead me to believe that there is a second transaction being created). Here is the test:

    [Test]
    public void Commit_ItemToCommitContainsStaleData_ThrowsStaleObjectStateException()
    {
        using (new TransactionScope())
        {
            // arrange
            RootUnitOfWorkFactory factory = CreateUnitOfWorkFactory();
            const int Id = 1;

            WorkItemRepository firstRepository = new WorkItemRepository(factory);
            WorkItem itemToChange = WorkItem.Create(Id);
            firstRepository.Commit(itemToChange);

            WorkItemRepository secondRepository = new WorkItemRepository(factory);
            WorkItem copyOfItemToChange = secondRepository.Get(Id);

            // act
            copyOfItemToChange.ChangeDescription("A");
            secondRepository.Commit(copyOfItemToChange);

            itemToChange.ChangeDescription("B");

            // assert
            Assert.Throws<StaleObjectStateException>(() => firstRepository.Commit(itemToChange));
        }
    }

This is the bottom of the error stack:

failed: NHibernate.Exceptions.GenericADOException : could not load an entity: [TfsTimeMachine.Domain.WorkItem#1][SQL: SELECT workitem0_.Id as Id1_0_, workitem0_.LastChanged as LastChan2_1_0_, workitem0_.Description as Descript3_1_0_ FROM [WorkItem] workitem0_ WHERE workitem0_.Id=?] ----> System.Data.SqlClient.SqlException : MSDTC on server 'ADM4200\SQLEXPRESS' is unavailable. at NHibernate.Loader.Loader.LoadEntity(ISessionImplementor session, Object id, IType identifierType, Object optionalObject, String optionalEntityName, Object optionalIdentifier, IEntityPersister persister).

I'm running NUnit 2.1, so can someone tell me if Nhibernate creates implicit transactions if there is no session.BeginTransaction() before querying data, regardless of the session running within a TransactionScope?

+1  A: 

I'm not sure if Hibernate is using transactions internally, but I also don't think that is your problem here.

It appears that the problem is that you are using two different data sources in the same transaction. In order to coordinate the transaction between both data sources for a two-phase commit, you would need to have DTC enabled. The fact that both data sources are actually the same database is immaterial.

Eric Petroelje
Yes, you are correct. So the answer is no, Nhibernate does not create implicit transactions. This fails because two different ADO.net connections enlists to the same transaction, so the get request on the second repository causes the error. Now i just need to control the session connection within a transactionscope (there should be no need for dist trans in my case) to get this to work
Marius
A: 

I got this to work. The problem was (as stated in my comment) that two concurrent sessions were started within the same transactionscope and both started a new dbconnection which enlisted the same transaction, thus forcing DTC to kick in. The solution to this was to create a custom connection provider which ensured that the same connection was returned while inside a transactionscope. I then put this into play in my test and presto, I could test stale object state and rollback the data when the tests completes. Heres my implementation:

/// <summary>
/// A connection provider which returns the same db connetion while
/// there exists a TransactionScope.
/// </summary>
public sealed class AmbientTransactionAwareDriverConnectionProvider : IConnectionProvider
{
    private readonly bool disposeDecoratedProviderWhenDisposingThis;
    private IConnectionProvider decoratedProvider;
    private IDbConnection maintainedConnectionThroughAmbientSession;

    public AmbientTransactionAwareDriverConnectionProvider()
        : this(new DriverConnectionProvider(), true)
    {}

    public AmbientTransactionAwareDriverConnectionProvider(IConnectionProvider decoratedProvider,
                                                      bool disposeDecoratedProviderWhenDisposingThis)
    {
        Guard.AssertNotNull(decoratedProvider, "decoratedProvider");
        this.decoratedProvider = decoratedProvider;
        this.disposeDecoratedProviderWhenDisposingThis = disposeDecoratedProviderWhenDisposingThis;
    }

    ~AmbientTransactionAwareDriverConnectionProvider()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public void Configure(IDictionary<string, string> settings)
    {
        this.decoratedProvider.Configure(settings);
    }

    public void CloseConnection(IDbConnection conn)
    {
        if (Transaction.Current == null)
            this.decoratedProvider.CloseConnection(conn);
    }

    public IDbConnection GetConnection()
    {
        if (Transaction.Current == null)
        {
            if (this.maintainedConnectionThroughAmbientSession != null)
                this.maintainedConnectionThroughAmbientSession.Dispose();

            return this.decoratedProvider.GetConnection();
        }

        if (this.maintainedConnectionThroughAmbientSession == null)
            this.maintainedConnectionThroughAmbientSession = this.decoratedProvider.GetConnection();

        return this.maintainedConnectionThroughAmbientSession;
    }

    private void Dispose(bool disposing)
    {
        if (this.maintainedConnectionThroughAmbientSession != null)
            CloseConnection(this.maintainedConnectionThroughAmbientSession);

        if (this.disposeDecoratedProviderWhenDisposingThis && this.decoratedProvider != null)
            this.decoratedProvider.Dispose();

        if (disposing)
        {
            this.decoratedProvider = null;
            this.maintainedConnectionThroughAmbientSession = null;
        }
    }

    public IDriver Driver
    {
        get { return this.decoratedProvider.Driver; }
    }

}
Marius