Story on how MiniProfiler 3 breaks TransactionScope flow

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.

via GIPHY

Chat with blog beta
āœ•
  • Assistant: Hello, I'm a blog assistant powered by GPT-3.5 Turbo. Ask me about the article.