Reputation: 1133
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!!");
}
}
Upvotes: 4
Views: 2369
Reputation: 12846
You could call your async methods within a HostingEnvironment.QueueBackgroundWorkItem
call.
HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
{
await LongRunningMethodAsync();
});
QueueBackgroundWorkItem is summarized as follows:
The HostingEnvironment.QueueBackgroundWorkItem method lets you schedule small background work items. ASP.NET tracks these items and prevents IIS from abruptly terminating the worker process until all background work items have completed.
Upvotes: 3
Reputation: 52240
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.
Upvotes: 5