How to support async methods in a TransactionScope with Microsoft.Bcl.Async in .NET 4.0?
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.
Related videos on Youtube
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, 2020Comments
-
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 almost 10 yearsYou can always take the source code, clean it up a bit and implement it yourself
-
noseratio almost 10 yearsIf scalability isn't a major factor, you can introduce thread affinity like that.
-
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
andPopScope
- which would ultimately require rewriting the whole assembly... -
Aron almost 10 yearsIs 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 almost 10 years@Aron - yes, this app has high performance and scalabilty requirements that async/await handles nicely.
-
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 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 breakTransactionScope.Dispose
(called when theusing
scope ends), under .NET 4.0. This is a known problem. -
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 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 almost 10 years@Noseratio oh...so you did.
-
noseratio almost 10 years@MattJohnson, is it an ASP.NET code, after all? Do you care about the current synchronization context?
-
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 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 almost 10 years@MattJohnson, you were right, unfortunately, overlapping
TransactionScope
fails even on the same threadSystem.InvalidOperationException: TransactionScope nested incorrectly
. I've just verified that and I'm deleting my answer. -
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 almost 10 yearsFYI - Customer upgraded to 4.5.1. The question is still valid for others, but I have no immediate need now.
-
-
Todd Menier over 9 yearsI 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 over 9 years@ToddMenier it doesn't explain the error above, then.
-
Henrik Hjalmarsson over 8 yearsAlthough I just saw you couldn't find that option for some reason, which is wierd.
-
Alexandre about 8 yearsImportant note: TransactionScopeAsyncFlowOption is available only on .net framework 4.5.1 or higher particular.net/blog/…
-
Matt Johnson-Pint over 4 yearsI'm marking this as the accepted answer, because .NET Framework 4.0 is no longer relevant. Thanks.
-
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 almost 4 yearsAlternatively ... 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 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 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)