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
andasync/await
together, you should really upgrade to .NET 4.5.1 right away.- A
TransactionScope
wrapping asynchronous code needs to specifyTransactionScopeAsyncFlowOption.Enabled
in its constructor.
🔗Transaction, wrap me up!
The System.Transactions.TransactionScope
class allows you to wrap your database code, your infrastructure code and sometimes even third-party code (if supported by the third-party library) inside a transaction. The code then only performs the actions when you actually want to commit (or complete) the transaction.
As long as all the code inside the TransactionScope
is executed on the same thread, all the code on the call stack can participate with the TransactionScope
defined in your code. You can nest scopes or create new independent scopes inside a parent transaction scope. You can even creates clones of a TransactionScope
and pass the clone to another thread and join back onto the calling thread. By wrapping your code with a transaction scope, you are using an implicit transaction model, also called ambient transactions.
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 propertyTransaction.Current
to a new instance of aCommittableTransaction
.
Traditional code using a TransactionScope
looks like this:
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);
}
As we can see outside the using
block the Transaction.Current
property is null. Inside the using
block the Transaction.Current
property is not null. Even a method in the call stack like SomeMethodInTheCallStack
can access Transaction.Current
as long as it is wrapped in the using
block.
The benefits of the TransactionScope
is that the local transaction automatically escalates to a distributed transaction if necessary. The scope also simplifies programming with transactions if you favor implicit over explicit.
🔗TransactionFlowInterruptedException
When async/await
was introduced with C# 5.0 and .NET 4.5, one tiny little detail was completely forgotten. The underlying state machine introduced by the compiler didn’t properly “float” the transaction around when an async
method was called under a wrapping TransactionScope
. Let’s apply our knowledge of how TransactionScope
works in synchronous code to asynchronous code. Now, if all we do is introduce an async
method call inside the scope, you might expect that it would work just the same:
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. We also these changes to our own products as well, updating the target framework for NServiceBus as part of our 6.0 release.
To learn more, check out our Async/Await webinar series, where you can learn how to avoid more common pitfalls when working with asynchronous code.
Be one with the flow!
About the author: Daniel Marbach is a solution architect at Particular Software, Microsoft Integration MVP, coach and passionate blogger.