Get Transactionscope to Work with Async/Await

TransactionScope enlistment with async/await

Based on comments on my answer, specifically Panagiotis Kanavos' answer:

The async option for transactionscope causes the TransactionScope to be passed from task to task via the synchronization context. The test code in my question opened the connection on a different thread than the TransactionScope was opened on with Task.StartNew, so there was no context to pass along, no transaction to propagate.

How to support async methods in a TransactionScope with Microsoft.Bcl.Async in .NET 4.0?

It is not possible to achieve this in .NET Framework 4.0. Additionally, .NET Framework 4.0 reached end of life on 2016-01-12, and thus is no longer relevant.

To support transaction scope in async methods in .NET going forward (since .NET Framework 4.5.1), use TransactionScopeAsyncFlowOption.Enabled

public static TransactionScope CreateAsyncTransactionScope(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
{
var transactionOptions = new TransactionOptions
{
IsolationLevel = isolationLevel,
Timeout = TransactionManager.MaximumTimeout
};
return new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled);
}

Enable Async TransactionScope without TransactionScopeAsyncFlowOption.Enabled

After fair amount of search across blogs and article, I have found the following blog by Stephen Toub, helps in achieving the Continuation of Async method on exactly same thread, thus avoiding the Transaction Scope issue. Now I don't need TransactionScopeAsyncFlowOption.Enabled to get the Async methods run in the TransactionScope

https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/

void Main()
{
// Modified Async Scheduler for Continuations to work on Exactly same thread
// Required in the case same Thread is required for Task Continuation post await
Run(async () => await DemoAsync());

"Main Complete".Dump();
}

static async Task DemoAsync()
{
// Transcation Scope test (shall dispose
using (var ts = new TransactionScope())
{
await Cache + Database Async update
ts.Complete();
"Transaction Scope Complete".Dump();
}
}

// Run Method to utilize the Single Thread Synchronization context, thus ensuring we can
// Control the threads / Synchronization context post await, cotinuation run of specific set of threads

public static void Run(Func<Task> func)
{
// Fetch Current Synchronization context
var prevCtx = SynchronizationContext.Current;

try
{
// Create SingleThreadSynchronizationContext
var syncCtx = new SingleThreadSynchronizationContext();

// Set SingleThreadSynchronizationContext
SynchronizationContext.SetSynchronizationContext(syncCtx);

// Execute Func<Task> to fetch the task to be executed
var t = func();

// On Continuation complete the SingleThreadSynchronizationContext
t.ContinueWith(
delegate { syncCtx.Complete(); }, TaskScheduler.Default);

// Ensure that SingleThreadSynchronizationContext run on a single thread
// Execute a Task and its continuation on same thread
syncCtx.RunOnCurrentThread();

// Fetch Result if any
t.GetAwaiter().GetResult();
}
// Reset the Previous Synchronization Context
finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
}

// Overriden Synchronization context, using Blocking Collection Consumer / Producer model
// Ensure that same Synchronization context / Thread / set of threads are maintained
// In this case we main a single thread for continuation post await

private sealed class SingleThreadSynchronizationContext : SynchronizationContext
{
// BlockingCollection Consumer Producer Model
private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>>
m_queue = new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();

// Override Post, which is called during Async continuation
// Send is for Synchronous continuation
public override void Post(SendOrPostCallback d, object state)
{
m_queue.Add(
new KeyValuePair<SendOrPostCallback, object>(d, state));
}

// RunOnCurrentThread, does the job if fetching object from BlockingCollection and execute it
public void RunOnCurrentThread()
{
KeyValuePair<SendOrPostCallback, object> workItem;
while (m_queue.TryTake(out workItem, Timeout.Infinite))
workItem.Key(workItem.Value);
}

// Compete the SynchronizationContext
public void Complete() { m_queue.CompleteAdding(); }
}

Best way to fire and forget async code inside TransactionScope

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.

Asynchronously commit or rollback a transaction scope

There's no way to implement it so far. But they work on it

How to dispose TransactionScope in cancelable async/await?

The problem stems from the fact that I was prototyping the code in a console application, which I did not reflect in the question.

The way async/await continues to execute the code after await is dependent on the presence of SynchronizationContext.Current, and console application don't have one by default, which means the continuation gets executed using the current TaskScheduler, which is a ThreadPool, so it (potentially?) executes on a different thread.

Thus one simply needs to have a SynchronizationContext that will ensure TransactionScope is disposed on the same thread it was created. WinForms and WPF applications will have it by default, while console applications can either use a custom one, or borrow DispatcherSynchronizationContext from WPF.

Here are two great blog posts that explain the mechanics in detail:

Await, SynchronizationContext, and Console Apps

Await, SynchronizationContext, and Console Apps: Part 2



Related Topics



Leave a reply



Submit