Allowing Transactions into Your Component: "Do you really want that?"

Supporting transactions is part of your component contract. If you shipped a component yesterday that exposed MyComp.DoWork to the public, and today you want to add transactional support to your component, you shouldn't use the same method MyComp.DoWork and make it transactions aware. Not unless you want to break your customers (see https://blogs.msdn.com/florinlazar/archive/2005/04/19/409834.aspx for details on how you can break existing apps). Instead you should publish a new method, MyComp.TransactedDoWork, that you document it as being transactions aware, i.e. it will participate in the ambient transaction if one is available.

The purpose of this post is not about TransactedDoWork, but rather about a new method, MyComp.DoMoreWork, that you want to add to your v2. Let's assume that in this method, you write an entry into a file, call a webservice and update two databases. Since you learned about System.Transactions, you are using TransactionScope to make the updates to the two databases. And since most of the samples use the default constructor for TransactionScope, you are probably doing it the same way:

 using (TransactionScope ts = new TransactionScope())
{
    // open connection to db1
    // update db1
    // close connection to db1
    // open connection to db2
    // update db2
    // close connection to db2
    
    ts.Complete();
}

All appears to be good and so you ship MyComp v2. After a while, you start getting reports from your customers saying that sometimes, MyComp.DoMoreWork doesn't work properly: they see the update to the file, they see the webservice being called but the updates to databases are missing. What is going on?

Well, the default constructor for TransactionScope uses TransactionScopeOption.Required, which enlists in the ambient transaction if one exists. Thus, if your customers are using transactions and they call MyComp.DoMoreWork, and if the transaction aborts, part of the work done by DoMoreWork is going to be rollbacked!! But that wasn't what you intended.

The best practice is to always "protect" your outermost TransactionScopes in our component from enlisting in the ambient transaction by using a non-default constructor,  TransactionScope(TransactionScopeOption.RequiresNew) or TransactionScope(TransactionScopeOption.Suppress), depending on your particular scenario. Of course, if you want the transaction to flow inside the component you can use the default constructor, but that should be a conscious decision.

Given that the TransactionScope is usually hidden into the call stack of your component method, for instance:

 void MyComp.DoMoreWork
{
    internalDoTheRealMoreWork(); // the first TransactionScope shows up inside internalDoTheRealMoreWork
}

The best way to deal with unwanted transactions is to start your public interface methods with a non-default TransactionScope:

 void MyComp.DoMoreWork
{
    using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Suppress))
    {
        internalDoTheRealMoreWork();       
        ts.Complete();
    }
}

The reason why TransactionScope default is Required is to allow easy creation of imbricated or nested TransactionScopes (not to be confused with "nested transactions"):

 void internalDoTheRealMoreWork()
{
    using (TransactionScope ts = new TransactionScope())
    {
        // work
        myOtherInternalWork();
        ts.Complete();
    }
}
void myOtherInternalWork();
{
    using (TransactionScope ts = new TransactionScope())
    {
        // do other work
        ts.Complete();
    } 
}

With the default constructor you write less code and the transactions "flows" nicely from method to method in the call stack. You are supposed to use the non-default constructors for TransactionScope, when you want to make a decision not to allow the ambient transaction inside your component or method. One of those cases is in the public interface methods.

Next time you write a public method, always ask yourself - "do I want to let a transaction in or not?" - if the answer is no, then start your public method with a non-default TransactionScope. And remember to document your method's behavior with regards to transactions.