TransactionScope and Async/Await. Be one with the flow!
You might not know this, but the 4.5.0 version of the .NET Framework contains a serious bug regarding System.Transactions.TransactionScope and how it behaves with async/await. Because of this bug, a TransactionScope can't flow through into your asynchronous continuations. This potentially changes the threading context of the transaction, causing exceptions to be thrown when the transaction scope is disposed.
This is a big problem, as it makes writing asynchronous code involving transactions extremely error-prone.
The good news is that as part of the .NET Framework 4.5.1, Microsoft released the fix for that "asynchronous continuation" bug. The thing is that developers like us now need to explicitly opt-in to get this new behavior. Let's take a look at how to do just that.
TL;DR
- If you are using TransactionScope and async/await together, you should really upgrade to .NET 4.5.1 right away.
- A TransactionScope wrapping asynchronous code needs to specify TransactionScopeAsyncFlowOption.Enabled in its constructor.
An explicit transaction means we create a new instance of a CommittableTransaction in code and pass it from method to method as a parameter. Ambient or implicit means we wrap a code inside a TransactionScope. This sets the thread-static property Transaction.Current to a new instance of a CommittableTransaction.
public void TransactionScopeAffectsCurrentTransaction() { Debug.Assert(Transaction.Current == null); using (var tx = new TransactionScope()) { Debug.Assert(Transaction.Current != null); SomeMethodInTheCallStack(); tx.Complete(); } Debug.Assert(Transaction.Current == null); } private static void SomeMethodInTheCallStack() { Debug.Assert(Transaction.Current != null); }
public async Task TransactionScopeWhichDoesntBehaveLikeYouThinkItShould() { using (var tx = new TransactionScope()) { await SomeMethodInTheCallStackAsync() .ConfigureAwait(false); tx.Complete(); } } private static async Task SomeMethodInTheCallStackAsync() { await Task.Delay(500).ConfigureAwait(false); }
Unfortunately, it doesn't work that way. The code almost (but only almost) executes similarly to the synchronous version, but if the project this code is written in targets .NET Framework 4.5, when we reach the end of the using block and try to Dispose the TransactionScope the following exception is thrown:
System.InvalidOperationException : A TransactionScope must be disposed on the same thread that it was created.
To make TransactionScope and async work properly we need to upgrade our project to .NET 4.5.1.
Let it flow
With .NET 4.5.1 the TransactionScope has a new enumeration called TransactionScopeAsyncFlowOption which can be provided in the constructor. You have to explicitly opt-in the transaction flow across thread continuations by specifying TransactionScopeAsyncFlowOption.Enabled like this:
using (var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { await SomeMethodInTheCallStackAsync() .ConfigureAwait(false); tx.Complete(); }
If you were wondering, the default TransactionScopeAsyncFlowOption is Suppress; Microsoft wanted to avoid breaking codebases that assumed the old .NET 4.5.0 behavior.
In the flow
Even if you are not using NServiceBus but TransactionScope combined with async/await you should update all your code paths that are using a TransactionScope to include TransactionScopeAsyncFlowOption.Enabled. This enables the transaction to properly flow into asynchronous code, preventing business logic from misbehaving when used under a TransactionScope. This will save you many headaches.