Limitation of TransactionScope (and using)

If you read the documentation for TransactionScope, you will find: "If no exception occurs within the transaction scope […], then the transaction in which the scope participates is allowed to proceed. If an exception does occur within the transaction scope, the transaction in which it participates will be rolled back. " Later the doc says: “When your application completes all work it wants to perform in a transaction, you should call the Complete method only once to inform that transaction manager that it is acceptable to commit the transaction . Failing to call this method aborts the transaction.”

 

OK, so how about this code:

 

using(TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew)) {

    // do transactional work

    ts.Complete();

    throw new Exception();

}

 

What happens to the transaction after executing this code? Take a guess. If we follow the doc, the transaction roll backs, right? Well, the transaction actually commits – try it out!

 

Let’s see what happens by “unpacking” the using statement:

     

TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew);

try {

    // do transactional work

 

    ts.Complete();

    throw new Exception();

}

finally {

    ts.Dispose();

}

 

In the Dispose of TransactionScope something similar to the following pseudo-code happens:

       if Complete was called {

              Transaction.Commit();

}

else {

       Transaction.Rollback();

}

The limitation really comes from the fact that at Dispose time in finally, the using construct doesn’t offer any information about completion of its body with exception or not. That is why the Transaction.Complete was invented and that is why the documentation for Complete says: “It is very good practice to put the call as the last statement in the using block.” Complete was invented to detect exceptions, i.e. if an exception occurs, Complete will not get called – but this work only if one follows the guideline to put Complete as the last statement. Doing any work after Complete was called is asking for trouble and possible data corruption. 

If you want to be on the safe side, use my transacted construct which doesn’t have these issues. It follows a simple rule: if an exception is thrown, the transaction aborts, otherwise it commits.