问题
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.
回答1:
You have to 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);
}
回答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.
回答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.
来源:https://stackoverflow.com/questions/24593070/how-to-support-async-methods-in-a-transactionscope-with-microsoft-bcl-async-in