How to dispose TransactionScope in cancelable async/await?

.net, async-await, c#, database, transactions

Solution

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

Problem

I'm trying to use the new async/await feature to asynchronously work with a DB. As some of the requests can be lengthy, I want to be able to cancel them. The issue I'm running into is that `TransactionScope` apparently has a thread affinity, and it seems that when canceling the task, its `Dispose()` gets ran on a wrong thread. Specifically, when calling `.TestTx()` I get the following `AggregateException` containing `InvalidOperationException` on `task.Wait ()`: ``` "A TransactionScope must be disposed on the same thread that it was created." ``` Here's the code: ``` public void TestTx () { var cancellation = new CancellationTokenSource (); var task = TestTxAsync ( cancellation.Token ); cancellation.Cancel (); task.Wait (); } private async Task TestTxAsync ( CancellationToken cancellationToken ) { using ( var scope = new TransactionScope () ) { using ( var connection = new SqlConnection ( m_ConnectionString ) ) { await connection.OpenAsync ( cancellationToken ); //using ( var command = new SqlCommand ( ... , connection ) ) { // await command.ExecuteReaderAsync (); // ... //} } } } ``` UPDATED: the commented out part is to show there's something to be done — asynchronously — with the connection once it's open, but that code is not required to reproduce the issue.

Original source

Related problems