Search code examples
c#.netasync-awaittransactionscopefire-and-forget

Best way to fire and forget async code inside TransactionScope


I'm doing some stuff inside a using block for a TransactionScope object. At some point I wanted to call some async code by firing and forget (I don't want to wait for the result, and I'm not interested in what happens during that call) and I wanted that code to not be part of the transaction (by using TransactionScopeOption.Suppress option).

So initially I made something similar to the methodFails that I have commented in the code below. It got me a nice "System.InvalidOperationException: 'TransactionScope nested incorrectly'". I looked up in SO for somebody having similar problems, and found this Question where the answer by ZunTzu gave me the idea for method1 using TransactionScopeAsyncFlowOption.Enabled option, which works as I expected for methodFails but without the exception.

Then I thought of an alternative that I put in method2 that consists in putting the async code in a third method (method3) called by firing-and-forget while the TransactionScopeOption.Suppress option is kept in the non-async method2. And this approach seems to work as good as method1 in my sample program.

So my question is: which approach is better, method1 or method2, or maybe a third one that I have no thought about? I'm leaning for method1 because it sounds like "the people making the TransactionScope class put that TransactionScopeAsyncFlowOption there for a reason". But the fact that TransactionScopeAsyncFlowOption.Enabled is not the default for a TransactionScope makes me think that maybe there is a performance hit by enabling that, and fire-and-forget may be a special case where I can save that performance hit.

The sample code:

    class Program
    {
        static void Main(string[] args)
        {
            using (TransactionScope scope1 = new TransactionScope())
            {
                // Do some stuff in scope1...

                // Start calls that could execute async code
                //Task a = methodFails(); // This commented method would launch exception: System.InvalidOperationException: 'TransactionScope nested incorrectly'
                Task b = method1(); // Fire and forget
                method2();

                // Rest of stuff in scope1 ...
            }
            Console.ReadLine();
        }

        static async Task methodFails()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress))
            {
                //Do non-transactional work here
                Console.WriteLine("Hello World 0.1!!");
                await Task.Delay(10000);
                Console.WriteLine("Hello World 0.2!!");
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 0.3!!");
        }

        static async Task method1()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
            {
                //Do non-transactional work here
                Console.WriteLine("Hello World 1.1!!");
                await Task.Delay(10000);
                Console.WriteLine("Hello World 1.2!!");
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 1.3!!");
        }

        static void method2()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress))
            {
                //Do non-transactional work here
                Task ignored = method3(); // Fire and forget
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 2.2!!");
        }

        static async Task method3()
        {
            //Do non-transactional work here
            Console.WriteLine("Hello World 2.1!!");
            await Task.Delay(10000);
            Console.WriteLine("Hello World 2.3!!");
        }
    }

Solution

  • But the fact that TransactionScopeAsyncFlowOption.Enabled is not the default for a TransactionScope makes me think that maybe there is a performance hit by enabling that, and fire-and-forget may be a special case where I can save that performance hit.

    TransactionScopeAsyncFlowOption.Enabled was introduced for backward compatibility purposes when they fixed a bug. Strangely, you don't benefit from the bug fix unless you "opt in" by setting this flag. They did it that way so the bug fix didn't break any existing code that relied on the buggy behavior.

    In this article:

    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.

    • A TransactionScope wrapping asynchronous code needs to specify TransactionScopeAsyncFlowOption.Enabled in its constructor.