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

14,191

Solution 1

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);
    }

Solution 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.

Share:
14,191

Related videos on Youtube

Matt Johnson-Pint
Author by

Matt Johnson-Pint

né Matt Johnson. He/him/his (not dude/bro, please). Hi! I am primarily a .NET C# developer, and I also do a lot of work in JavaScript. I specialize in date and time issues, especially the tricky ones involving time zones. I currently work for Sentry, primarily on the .NET SDK. I used to work for Microsoft, and other companies prior to that. All questions, answers, comments and code are from me personally, and in no way represented the opinions of my past or present employers.

Updated on July 09, 2020

Comments

  • Matt Johnson-Pint
    Matt Johnson-Pint almost 4 years

    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.

    • Yuval Itzchakov
      Yuval Itzchakov almost 10 years
      You can always take the source code, clean it up a bit and implement it yourself
    • noseratio
      noseratio almost 10 years
      If scalability isn't a major factor, you can introduce thread affinity like that.
    • Matt Johnson-Pint
      Matt Johnson-Pint almost 10 years
      @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...
    • Aron
      Aron almost 10 years
      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.
    • Matt Johnson-Pint
      Matt Johnson-Pint almost 10 years
      @Aron - yes, this app has high performance and scalabilty requirements that async/await handles nicely.
    • Aron
      Aron almost 10 years
      @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.
    • noseratio
      noseratio almost 10 years
      @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.
    • Aron
      Aron almost 10 years
      @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?
    • noseratio
      noseratio almost 10 years
      @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, but the scalability issue would remain.
    • Aron
      Aron almost 10 years
      @Noseratio oh...so you did.
    • noseratio
      noseratio almost 10 years
      @MattJohnson, is it an ASP.NET code, after all? Do you care about the current synchronization context?
    • Matt Johnson-Pint
      Matt Johnson-Pint almost 10 years
      @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...
    • noseratio
      noseratio almost 10 years
      @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
      noseratio almost 10 years
      @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.
    • Matt Johnson-Pint
      Matt Johnson-Pint almost 10 years
      @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
      Matt Johnson-Pint almost 10 years
      FYI - Customer upgraded to 4.5.1. The question is still valid for others, but I have no immediate need now.
  • Todd Menier
    Todd Menier over 9 years
    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.
  • AgentFire
    AgentFire over 9 years
    @ToddMenier it doesn't explain the error above, then.
  • Henrik Hjalmarsson
    Henrik Hjalmarsson over 8 years
    Although I just saw you couldn't find that option for some reason, which is wierd.
  • Alexandre
    Alexandre about 8 years
    Important note: TransactionScopeAsyncFlowOption is available only on .net framework 4.5.1 or higher particular.net/blog/…
  • Matt Johnson-Pint
    Matt Johnson-Pint over 4 years
    I'm marking this as the accepted answer, because .NET Framework 4.0 is no longer relevant. Thanks.
  • Brondahl
    Brondahl almost 4 years
    @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
    Brondahl almost 4 years
    Alternatively ... please edit the question & title to remove the .NET 4.0 limitation. ... at which point it'll get closed as a dupe of stackoverflow.com/questions/13543254/…
  • Matt Johnson-Pint
    Matt Johnson-Pint almost 4 years
    @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.
  • Brondahl
    Brondahl almost 4 years
    @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)