129

I'm trying to integrate async/await into our service bus. I implemented a SingleThreadSynchronizationContext based on this example http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx.

And it works fine, except for one thing: TransactionScope. I await for stuff inside the TransactionScope and it breaks the TransactionScope.

TransactionScope doesn't seem to play nice with async/await, certainly because it stores things in the thread using ThreadStaticAttribute. I get this exception:

"TransactionScope nested incorrectly.".

I tried to save TransactionScope data before queuing the task and restore it before running it, but it doesn't seem to change a thing. And TransactionScope code is a mess, so it's really hard to understand what's going on there.

Is there a way to make it work? Is there some alternative to TransactionScope?

Pang
  • 9,564
  • 146
  • 81
  • 122
Yann
  • 1,388
  • 2
  • 11
  • 9
  • Here is a very simple code to reproduce a TransactionScope error http://pastebin.com/Eh1dxG4a except that the exception here is Transaction Aborted – Yann Nov 24 '12 at 17:31
  • Can you nit just use a regular SQL transaction? Or are you spanning multiple resources? – Marc Gravell Nov 24 '12 at 17:43
  • I'm spanning multiple ressources – Yann Nov 24 '12 at 19:27
  • Looks like you'll need to either pass the scope into your async method, or give it a way to retrieve it from some sort of common context that is identified with your work unit. – Bertrand Le Roy Nov 24 '12 at 19:43
  • You'll need a separate thread with its own `SingleThreadSynchronizationContext` for each top-level `TransactionScope`. – Stephen Cleary Nov 25 '12 at 14:24
  • I was trying to save the ThreadStatic Scope context in the SynchronisationContext Post method. But this method is executed on the same Thread as the Method that is posted. So now I save the context just after declaring the TransactionScope and it works. Of course I'm accessing the scope ContextData using reflection, and this is very wrong. But at least it works ! – Yann Nov 25 '12 at 17:17

4 Answers4

185

In .NET Framework 4.5.1, there is a set of new constructors for TransactionScope that take a TransactionScopeAsyncFlowOption parameter.

According to the MSDN, it enables transaction flow across thread continuations.

My understanding is that it is meant to allow you to write code like this:

// transaction scope
using (var scope = new TransactionScope(... ,
  TransactionScopeAsyncFlowOption.Enabled))
{
  // connection
  using (var connection = new SqlConnection(_connectionString))
  {
    // open connection asynchronously
    await connection.OpenAsync();

    using (var command = connection.CreateCommand())
    {
      command.CommandText = ...;

      // run command asynchronously
      using (var dataReader = await command.ExecuteReaderAsync())
      {
        while (dataReader.Read())
        {
          ...
        }
      }
    }
  }
  scope.Complete();
}
Liam
  • 27,717
  • 28
  • 128
  • 190
ZunTzu
  • 7,244
  • 3
  • 31
  • 39
15

Bit late for an answer but I was having the same issue with MVC4 and I updated my project from 4.5 to 4.5.1 by right clicking on project go to properties. Select application tab change target framework to 4.5.1 and use transaction as follow.

using (AccountServiceClient client = new AccountServiceClient())
using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
}
abatishchev
  • 98,240
  • 88
  • 296
  • 433
Atul Chaudhary
  • 1,113
  • 1
  • 15
  • 15
6

You can use DependentTransaction created by Transaction.DependentClone() method:

static void Main(string[] args)
{
  // ...

  for (int i = 0; i < 10; i++)
  {

    var dtx = Transaction.Current.DependentClone(
        DependentCloneOption.BlockCommitUntilComplete);

    tasks[i] = TestStuff(dtx);
  }

  //...
}


static async Task TestStuff(DependentTransaction dtx)
{
    using (var ts = new TransactionScope(dtx))
    {
        // do transactional stuff

        ts.Complete();
    }
    dtx.Complete();
}

Managing Concurrency with DependentTransaction

http://adamprescott.net/2012/10/04/transactionscope-in-multi-threaded-applications/

maximpa
  • 1,958
  • 13
  • 16
  • 2
    Adam Prescott's example child task was not marked async. If you replace "do transactional stuff" with something like `await Task.Delay(500)` this pattern will also fail with `TransactionScope nested incorrectly` because the outermost TransactionScope (not shown in the above example) exits scope before the child task properly completes. Replace `await` with `Task.Wait()` and it works, but then you've lost the benefits of `async`. – mdisibio Apr 02 '14 at 00:01
  • This is a harder way to solve the problem. TransactionScope is to hide all that plumbing. – Eniola Mar 28 '16 at 17:05
1

In case anyone still follows this thread...

Microsoft.Data.SqlClient v3.0.1 looks like it fixes the deadlock issues with System.Transactions in async/await functions for .NET Framework! I'm using 4.8. I used the explicit distributed transaction method to allow for parallel query execution against a single sql server by using multiple connections, but I'm sure the behavior with TransactionScope is better too.

Create a CommittableTransaction, create any number of SqlConnections that enlist a DependentTransaction created from the parent committable, execute queries on the connections in parallel, complete the dependents after query execution, then commit/rollback the parent committable.

I tested it with 3 parallel async inserts to the same db on 3 different connections. While the parent committable is in progress, I used a messagebox to prompt for commit or rollback. While the distributed transaction is active, the tables were locked. I couldn't select from them in ssms. After selecting commit or rollback, either worked as expected.

Last week with Microsoft.Data.SqlClient 3.0.0, this was impossible because the .Commit() method would deadlock in an async function. I even tried BeginCommit/EndCommit with various methods. There was even a problem enlisting dependents in async functions, but that is fixed too. Now the simplest explicit distributed transaction method works with async/await.

  • I may have spoken too soon about enlisting dependents being fixed. After a lot of testing today, SqlConnection .EnlistTransaction() seems to randomly cause exceptions. Once you get an exception, you'll see messages about data readers being open and needing to be closed (i'm not using them at all for these tests) or cannot enlist because the transaction is in a bad state on every consecutive run. Sometimes waiting a few minutes allows a successful attempt, and sometimes I have to reset the DTC service before it will work again. At least there are no deadlocks. – Don Reynolds Sep 28 '21 at 05:06
  • My suspicion is that when you open and enlist a bunch of sql connections asynchronously, enlist will sometimes fail because dtc can't properly promote the transaction or can't add the new connections to the transaction after one or more of them started executing sql code. If I put an artificial 2 second pause after each call to EnlistTransaction() to give all the simultaneously created connections a chance to enlist before executing queries, the same code will work. – Don Reynolds Sep 28 '21 at 05:55