System.Transactions.TransactionScope provides an implicit programming model in which the transactions are automatically managed by the infrastructure. It provides a simple mechanism for you to specify a code block to participate in a transaction. TransactionScope reduces the complexity of code that need to use transactions and it allows existing transaction providers to be retrofitted to participate in this programming model. Nested transaction work transparently. Disparate transaction providers can participate in a transaction without increasing the complexity of your code. No wonder TransactionScope is so popular!
I have used TransactionScope countless times in the past, but it never occurred to me that there is such an elegant mechanism implemented under the hood. It proves that the team that worked on System.Transactions met its goals. Recently, while working on a project using TransactionScope, I realized that I don’t fully understand how TransactionScope really works. Look at it a little bit closely and you too will realize that it seems almost magical.
Without TransactionScope, you have to manage the transaction yourself. Your code is intimately aware of the transaction.
Now take a look at the code below that uses TransactionScope. Compare this to the code above.
This will work even if the two sql commands are executed in two different methods!
Note that the two methods have no transaction related code at all. A new connection is opened in each one of the two methods : ExecuteCommandA and ExecuteCommandB and sql commands are executed on those two connections. The changes are made by both commands will be rolled back if the second command fails. Note that none of the connections are explicitly associated with any transaction. How does this work? How does SqlConnection know about the implicit transaction? How does TransactionScope provides an instance of System.Transaction to the code encompassed by the TransactionScope block?
TransactionScope does its magic by providing and managing an “ambient” transaction, and System.Data.SqlClient is a System.Transaction resource manager. Which means that it is aware of the ambient transaction. Anyone can write an ambient transaction aware resource provider. The magic happens with the cooperation of the two, the ambient transaction provided by the TransactionScope and the implementation in System.Data.SqlClient that detects and honors the ambient transaction.
The ambient transaction provided by the TransactionScope is a thread-static (TLS) variable. It can be accessed with static Transaction.Current property. Here is the TransactionScope code at referencesource.microsoft.com. ThreadStatic ContextData, contains the CurrentTransaction.
When a SqlConnection is opened, it detects this ambient transaction and enlists itself in this transaction. If another connection is opened in another method while the transaction scope has not been disposed yet, it will also enlist itself. I am simplifying this to make a point, but whether a new transaction is created or the current ambient one is used, depends on the TransactionScopeOption.
So remember, the connection must be opened inside the TransactionScope block for it to enlist in the ambient transaction automatically. If the connection was opened before that, it will not participate in the transaction. Read this last sentence again. This is your only warning! TransactionScope or SqlConnection does not know or care and it will not warn you. This is especially important if have a pattern for sharing a connection instance among your methods (which is not a good idea anyway).
If you are wondering whether MSDTC (Microsoft Distributed Transaction Coordinator) will be used if the two connections are pointing to two different databases, then the answer is yes, absolutely. The transactions are promoted automatically to distributed transactions when needed and the MSDTC becomes involved. TransactionScope does not obviate the need for MSDTC. Make sure MSDTC is enabled on the servers where your code is executing, if your transactions will be spanning more than one database.
Remember that Transaction.Current is a thread static variable. If your code is executing in a multi-threaded environments, you may need to take some precautions. The connections that need to participate in ambient transactions must be opened on the same thread that creates the TransactionScope that is managing that ambient transaction.
Lastly, if you are using async/await inside the TransactionScope block, you should know that it does not work well with TransactionScope and you might want to look into new TransactionScope constructor in .NET Framework 4.5.1 that accepts a TransactionScopeAsyncFlowOption. TransactionScopeAsyncFlowOption.Enabled option, which is not the default, allows TransactionScope to play well with asynchronous continuations.
Hope this demystifies the magic of TransactionScope for you.