Skip to main content

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.

🔗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 property Transaction.Current to a new instance of a CommittableTransaction.

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.

Share on Twitter
Don't miss a thing. Sign up today and we'll send you an email when new posts come out.
Thank you for subscribing. We'll be in touch soon.
 
We collect and use this information in accordance with our privacy policy.