Can Effort be used with nested transactions?

Aug 26, 2015 at 9:13 AM
I'm currently working on a legacy codebase largely relying on
  • Database-first model, EF4, using ObjectContext
  • Nested transaction scopes, mostly with ReqiuresNew-option, i.e. not the ambient transaction
  • IsolationLevel Snapshot
I would like to remove the need for maintaining an actual physical DB for the integration tests,, and rather use Effort.

However, from a few proof-of-concept tests, it seems I will have the following problems
  • Effort.ObjectContextFactory.CreateTransient will not really work with nested transactions, as each ObjectContext will be a separate database and thus the effect of an inner transacion will be lost as soon as the related ObjectContext is disposed.
  • Effort.ObjectContextFactory.CreatePersistent will mean integration tests will share state, since the unit test host process is alive during execution of all unit tests (our current integration tests use a hacky method of opening an encapsulating transaction, and then tricking the code being tested into enlistiing in the ambient transaction instead of creating nested ones, but we would like to be able to actually test with real nested transactions) .
  • Using Effort.ObjectContextFactory.CreatePersistent, I get a lot of timeout problems when disposing the transactions, that don't happen when using a real SQL Server Express 2008-database.
For example, the following test will time out on disponsing the outer transaction scope when using Effort.ObjectContextFactory.CreatePersistent, but works with the real ObjectContext :
[TestClass]
    public class EffortIsolationLevelTests
    {
        [TestMethod]
        public void ShouldBeAbleToWriteInNestedTransactions()
        {
            using (var outerScope = CreateTransactionScope())
            {
                using (var outerContext = CreateObjectContext())
                {
                    var outerTransactionIdentifier = Transaction.Current.TransactionInformation.LocalIdentifier;
                    CreatePhoneNumberTypeAndAddToContext(outerContext, "Some type of phone number");
                    using (var innerScope = CreateTransactionScope())
                    {
                        using (var innerContext = CreateObjectContext())
                        {
                            Assert.AreNotEqual(outerTransactionIdentifier,
                                Transaction.Current.TransactionInformation.LocalIdentifier,
                                "Should use separate transactions");
                            CreatePhoneNumberTypeAndAddToContext(innerContext, "Some other type of phone number");
                        }
                        innerScope.Complete();
                    }
                }
                outerScope.Complete();
            }
        }

        private static void CreatePhoneNumberTypeAndAddToContext(NorthwindEntities context, string name)
        {
            var type = new PhoneNumberType() {Name = name, ModifiedDate = DateTime.Now};
            AddToContextAndSaveChanges(context, type);
        }

        private static void AddToContextAndSaveChanges(NorthwindEntities context, PhoneNumberType phoneNumberType)
        {
            context.AddToPhoneNumberTypes(phoneNumberType);
            context.SaveChanges();
        }

        private static TransactionScope CreateTransactionScope()
        {
            return new TransactionScope(TransactionScopeOption.RequiresNew,
                new TransactionOptions() {IsolationLevel = IsolationLevel.ReadCommitted, Timeout = new TimeSpan(0, 0, 5)});
        }

        private NorthwindEntities CreateObjectContext()
        {
            //This causes a timeout
            return Effort.ObjectContextFactory.CreatePersistent<NorthwindEntities>();
            //It works with the real context...
            //return new NorthwindEntities(); 
        }
    }
Test method EffortIsolationLevelTest.EffortIsolationLevelTests.ShouldBeAbleToWriteInNestedTransactions threw exception: 
System.Transactions.TransactionAbortedException: The transaction has aborted. ---> System.TimeoutException: Transaction Timeout

System.Transactions.TransactionStateAborted.BeginCommit(InternalTransaction tx, Boolean asyncCommit, AsyncCallback asyncCallback, Object asyncState)
System.Transactions.CommittableTransaction.Commit()
System.Transactions.TransactionScope.InternalDispose()
System.Transactions.TransactionScope.Dispose()
EffortIsolationLevelTest.EffortIsolationLevelTests.ShouldBeAbleToWriteInNestedTransactions() in <path_hidden>\Effort\EffortIsolationLevel\EffortIsolationLevelTest\UnitTest1.cs: line 38

So I guess my question is; is it too ambitious to use Effort for testing code relying on nested transactions, but at the same time requiring reset of DB state for each integration test?

Are there any known bugs/planned fixes relating to nested transaction scopes?
Dec 21, 2015 at 10:55 AM
For future reference; for transient entities, I solved my problems by faking the EntityConnection instead of the ObjectContext,.
The problem with timeouts on disposing the transactions when using persistent connections still applies though:
using System;
using System.Data.EntityClient;
using System.Linq;
using System.Transactions;
using EffortIsolationDataLayer;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace EffortIsolationLevelTest
{
    [TestClass]
    public class EffortIsolationLevelTests
    {
        private EntityConnection _connection;

        [TestInitialize]
        public void InitTests()
        {
            _connection = Effort.EntityConnectionFactory.CreateTransient("name=NorthwindEntities");
            //Using persistent still causes timeouts on disposing
            //_connection = Effort.EntityConnectionFactory.CreatePersistent("name=NorthwindEntities");
        }

        [TestMethod]
        public void ShouldBeAbleToWriteInNestedTransactions()
        {
            using (var outerScope = CreateTransactionScope())
            {
                using (var outerContext = CreateObjectContext())
                {
                    var outerTransactionIdentifier = Transaction.Current.TransactionInformation.LocalIdentifier;
                    CreatePhoneNumberTypeAndAddToContext(outerContext, "Some type of phone number");
                    using (var innerScope = CreateTransactionScope())
                    {
                        using (var innerContext = CreateObjectContext())
                        {
                            Assert.AreNotEqual(outerTransactionIdentifier,
                                Transaction.Current.TransactionInformation.LocalIdentifier,
                                "Should use separate transactions");
                            CreatePhoneNumberTypeAndAddToContext(innerContext, "Some other type of phone number");
                        }
                        innerScope.Complete();
                    }
                }
                outerScope.Complete();
            }

            using (var finalScope = CreateTransactionScope())
            {
                using (var finalContext = CreateObjectContext())
                {
                    Assert.AreEqual(2, finalContext.PhoneNumberTypes.Count(),
                        "Final transaction should be able to read data from the other completed transactions");
                }
            }
        }

        private static void CreatePhoneNumberTypeAndAddToContext(NorthwindEntities context, string name)
        {
            var type = new PhoneNumberType() {Name = name, ModifiedDate = DateTime.Now};
            AddToContextAndSaveChanges(context, type);
        }

        private static void AddToContextAndSaveChanges(NorthwindEntities context, PhoneNumberType phoneNumberType)
        {
            context.AddToPhoneNumberTypes(phoneNumberType);
            context.SaveChanges();
        }

        private static TransactionScope CreateTransactionScope()
        {
            return new TransactionScope(TransactionScopeOption.RequiresNew,
                new TransactionOptions() {IsolationLevel = IsolationLevel.ReadCommitted, Timeout = new TimeSpan(0, 0, 5)});
        }

        private NorthwindEntities CreateObjectContext()
        {
            return new NorthwindEntities(_connection);
        }
    }

}