30

I have a method similar to:

public async Task SaveItemsAsync(IEnumerable<MyItem> items)
{
    using (var ts = new TransactionScope())
    {
        foreach (var item in items)
        {
            await _repository.SaveItemAsync(item);
        }

        await _repository.DoSomethingElse();

        ts.Complete();
    }
}

This of course has issues because TransactionScope doesn't play nice with async/await.

It fails with an InvalidOperationException with the message:

"A TransactionScope must be disposed on the same thread that it was created."

I read about TransactionScopeAsyncFlowOption in this answer, which appears to be exactly what I need.

However, for this particular project, I have a hard requirement to support .Net 4.0 and cannot upgrade to 4.5 or 4.5.1. Thus the async/await behavior in my project is provided by the Microsoft.Bcl.Async NuGet Package.

I can't seem to find TransactionScopeAsyncFlowOption in this or any other OOB package. Am I just missing it somewhere?

If it is not available, is there an alternative for achieving the same result? That is - I would like the transaction scope to properly complete or rollback, despite crossing threads with continuations.

I added DoSomethingElse in the example above to illustrate that there may be multiple calls to make within the transaction scope, so simply passing all items to the database in one call is not a viable option.

In case it matters, the repository uses direct ADO.Net (SqlConnection, SqlCommand, etc) to write to a SQL Server.

UPDATE 1

I thought I had a solution which involved taking System.Transactions.dll from .Net 4.5.1 and including it in my project. However, I found that this worked only on my dev box because it already had 4.5.1 installed. It did not work when deploying to a machine with only .Net 4.0. It just gave a MissingMethodException. I'm looking for a solution that will work on a .Net 4.0 installation.

UPDATE 2

I originally asked this question in July 2014. .NET Framework 4.0, 4.5, and 4.5.1 reached end of life in January 2016. The question thus is no longer applicable and is here only for historical reference.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • 1
    You can always take [the source code](http://referencesource.microsoft.com/#System.Transactions/NetFx20/System.Transactions/System/Transactions/TransactionScope.cs), clean it up a bit and implement it yourself – Yuval Itzchakov Jul 06 '14 at 07:13
  • If scalability isn't a major factor, you can introduce thread affinity [like that](http://stackoverflow.com/questions/20993007/how-to-use-non-thread-safe-async-await-apis-and-patterns-with-asp-net-web-api). – noseratio Jul 06 '14 at 09:50
  • @YuvalItzchakov - Yes, I can see how it's implemented in System.Transactions, but implementing it myself would require rewriting internal private methods like `PushScope` and `PopScope` - which would ultimately require rewriting the whole assembly... – Matt Johnson-Pint Jul 06 '14 at 20:22
  • Is there any reason you are using threading? Shirley if you used single threaded async you would have no issues. Since you are using the SqlServer connection classes they support TAP async. – Aron Jul 07 '14 at 18:35
  • @Aron - yes, this app has high performance and scalabilty requirements that async/await handles nicely. – Matt Johnson-Pint Jul 08 '14 at 01:50
  • @MattJohnson I am asking you WHY you are using threads. If you stick to single threaded async it should not be a problem. Threading can actually hurt performance and scalability. – Aron Jul 08 '14 at 01:54
  • @Aron, the OP does use TAP. However, the continuation after `await _repository.SaveItemAsync(item)` will run on a different thread (most likely), which will break `TransactionScope.Dispose` (called when the `using` scope ends), under .NET 4.0. This is a [known problem](http://stackoverflow.com/q/20993007/1768303). – noseratio Jul 08 '14 at 02:21
  • @Noseratio "However, the continuation after await `_repository.SaveItemAsync(item)` will run on a different thread (most likely)" My point is, why not rewrite that continuation to use the main thread then? – Aron Jul 08 '14 at 02:25
  • @Aron, that's the approach I proposed earlier in the comments. It may however hurt scalability and it installs a custom synchronization context, so the initial synchronization context will not available for the continuation (which might be a problem for ASP.NET e.g.). The latter can possibly be addressed [with a custom awaiter](http://stackoverflow.com/q/18284998/1768303), but the scalability issue would remain. – noseratio Jul 08 '14 at 02:32
  • @Noseratio oh...so you did. – Aron Jul 08 '14 at 02:33
  • @MattJohnson, is it an ASP.NET code, after all? Do you care about the current synchronization context? – noseratio Jul 08 '14 at 02:38
  • 1
    @Noseratio - No, it's not ASP.Net. It's a custom Windows Service application. I'm interested if you could provide an answer that illustrates the technique you're proposing for this minimal example. I looked at your other links but I'm not making the connection... – Matt Johnson-Pint Jul 08 '14 at 04:57
  • @MattJohnson, I'll post an example when I got a few spare mins. It'd pretty much a copy of my code code from that link, but I hope we should be able to work out the connection to your case. I don't think scalability will be an issue if that pattern is used correctly. – noseratio Jul 08 '14 at 05:02
  • @MattJohnson, you were right, unfortunately, overlapping `TransactionScope` fails even on the same thread `System.InvalidOperationException: TransactionScope nested incorrectly`. I've just verified that and I'm deleting my answer. – noseratio Jul 08 '14 at 20:03
  • 2
    @Noseratio - That's a shame. Nice try though, and thanks for testing. I'm considering a few different techniques. 1) Manipulating TLS. 2) Explicitly passing the transaction around. 3) Convincing the customer to upgrade to 4.5.1. I think #3 is the best route. :) – Matt Johnson-Pint Jul 08 '14 at 22:08
  • FYI - Customer upgraded to 4.5.1. The question is still valid for others, but I have no immediate need now. – Matt Johnson-Pint Aug 03 '14 at 19:20

3 Answers3

39

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);
    }
Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • 1
    Although I just saw you couldn't find that option for some reason, which is wierd. – Henrik Hjalmarsson Nov 20 '15 at 10:31
  • 4
    Important note: TransactionScopeAsyncFlowOption is available only on .net framework 4.5.1 or higher http://particular.net/blog/transactionscope-and-async-await-be-one-with-the-flow – Alexandre May 03 '16 at 19:34
  • I'm marking this as the accepted answer, because .NET Framework 4.0 is no longer relevant. Thanks. – Matt Johnson-Pint Jan 10 '20 at 17:26
  • @MattJohnson-Pint This answer does NOT answer the question as asked, nor as titled. I think the question you've asked is a really good one! And whilst "upgrade to 4.5.1 and negate the question" is of course the correct fix, others in the future may not be able to do that. I think PitJ's answer below is one that others may need in the future and the one that makes this question useful to the community. – Brondahl Jul 08 '20 at 11:30
  • Alternatively ... please edit the question & title to remove the .NET 4.0 limitation. ... at which point it'll get closed as a dupe of https://stackoverflow.com/questions/13543254/get-transactionscope-to-work-with-async-await?noredirect=1&lq=1 – Brondahl Jul 08 '20 at 11:31
  • @Brondahl - I have edited the answer to frame it such that it answers the question and still provides details the community can use going forward. I've also closed it as a dup of the question you pointed at, as that one came first. Thanks for helping clean up years-old answers so they make sense today. I appreciate it. – Matt Johnson-Pint Jul 08 '20 at 16:36
  • @MattJohnson-Pint meh - I didn't do the work; I just posted a whingey comment telling you that you should do it ;) Thanks for updating (and for having acquired an answer for my question for me before I got here :D) – Brondahl Jul 08 '20 at 17:02
2

TransactionScope was fixed in framework 4.5.1 in regards of disposing async/await operations. Do not use with 4.5!!!

Use EF6 with DbContextTransaction as alternative.

using (Entities entities = new Entities())
    using (DbContextTransaction scope = entities.Database.BeginTransaction())
    {
        entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable)");
        scope.Commit();
    }

More info:

TransactionScope and Async/Await. Be one with the flow! Written by Daniel Marbach on August 6, 2015 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.

TL;DR

If you are using TransactionScope and async/await together, you should really upgrade to .NET 4.5.1 right away. A TransactionScope wrapping asynchronous code needs to specify TransactionScopeAsyncFlowOption.Enabled in its constructor.

Pit J
  • 169
  • 8
-3

Not sure if this fits your scenario but ConfigureAwait(false) can be used in an ASP.NET app to make sure an awaited function call re-enters the calling request context.

So if this code is running in an ASP.NET app the following code:

await _repository.SaveItemAsync(item).ConfigureAwait(false);

Would ensure that execution would continue on the request thread.

jaywayco
  • 5,846
  • 6
  • 25
  • 40
  • 13
    I believe you've got that backwards. `ConfigureAwait(true)` (the default, hence you don't have to specify it) means to sync back up to the original context in the continuation. `ConfigureAwait(false)` should be specified when you *don't* need that context and want to avoid the extra overhead of arranging it. – Todd Menier Oct 08 '14 at 15:14
  • 1
    @ToddMenier it doesn't explain the error above, then. – AgentFire Nov 25 '14 at 21:08