How to support async methods in a TransactionScope

2019-03-17 12:05发布

问题:

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

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.

回答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:

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.