Usually, while investigating some strange behavior I feel like I’m the detective Columbo and the murderer at the same time :)
Debugging is like being the detective in a crime movie where you are also the murderer — Filipe Fortes
This time, I noticed strange behavior on how TransactionScope
flows. More interesting, it works differently across our applications. So the simple code below works strange - in some apps Transaction.Current
is null
as expected, in others it is not. As a result, we are getting inconsistent data in the database.
using (var tran = new TransactionScope())
{
AsyncPump.Run(async () =>
{
await MakeDatabaseCall().ConfigureAwait(false);
// Transaction.Current should be null here
});
tran.Complete();
}
AsyncPump
is a small class written by Stephen Toub from .NET team at Microsoft. AsyncPump
helps to run async
code right in synchronous methods, it makes sense mostly in legacy systems when refactoring is not an option.
Since TransactionScope
does not receive TransactionScopeAsyncFlowOption.Enabled
parameter to support async
/await
flow, Transaction.Current
must be null
after any await
. Right? Of course! I thought this way but it turned out it is not always true.
MakeDatabaseCall
is doing the simplest database call select 1
.
private async Task MakeDatabaseCall()
{
using (var con = CreateDbConnection())
{
await con.OpenAsync();
var cmd = con.CreateCommand();
cmd.CommandText = "select 1;";
await cmd.ExecuteScalarAsync();
}
}
In my specific case, TransactionScope
should have supported async
/await
flow so I fixed this strange behavior by passing TransactionScopeAsyncFlowOption.Enabled
parameter into TransactionScope
to explicitly state my intention.
The issue is fixed but I still could not get rid of the thought why it worked differently. So I have spent a day debugging to understand who is the murderer. Eventually, the problem was found in a not expected place at all, it is CreateDbConnection
.
private DbConnection CreateDbConnection()
{
var connection = new SqlConnection("server=localhost;database=tempdb;UID=sa;PWD=***");
if (_useMiniProfiler)
return new ProfiledDbConnection(connection, MiniProfiler.Current);
return connection;
}
Some of our apps use MiniProfiler 3 to measure SQL query durations, see more details in How to alert on long-running SQL queries in .NET article. ProfiledDbConnection
wraps an instance of SqlConnection
and breaks it violating Liskov Substitution Principle. However, 4+
version of MiniProfiler
is OK and does not have such side effect. I would not call this is a bug because it appears in certain conditions: TransactionScope
+ AsyncPump
+ ConfigureAwait(false)
+ MiniProfiler 3
. I hit the jackpot!
Finally, the murderer is found so I can sleep peacefully :)
I managed to reproduce the behavior difference in MiniProfilerReproductionTests. MiniProfiler GitHub issue is here.