question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Wrong isolation level with Sql Azure and TransactionScope

See original GitHub issue

Describe the bug

On SQL Server, executing two queries inside a common TransactionScope, both are executed using the isolation level defined in the TransactionScope, as expected.

The same does not happen on SQL Azure: the second query is executed with the default Azure isolation level that is “Read Committed Snapshot”.

To reproduce

Here the code to reproduce the issue.

  class Program
  {
    const string AzureConnectionString = "Server=tcp:xxxx.database.windows.net,1433;Initial Catalog=xxxx; Persist Security Info=False;User ID=xxx; Password=xxx;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout = 30;";
    const string SqlServerConnectionString = "Server=localhost,1433;Initial Catalog=LCMS;Integrated Security=True";

    static void Main(string[] args)
    {
      Console.WriteLine("SQL SERVER");
      string connectionString = SqlServerConnectionString;
      using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions() { IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted }))
      {
        TestExec(connectionString); // Expected print: "read uncommitted"
        TestExec(connectionString); // Expected print: "read uncommitted"
      }

      Console.WriteLine("SQL AZURE");
      connectionString = AzureConnectionString;
      using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions() { IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted }))
      {
        TestExec(connectionString); // Expected print: "read uncommitted"
        TestExec(connectionString); // Expected print: "read uncommitted", Actual: "read committed snapshot"
      }
    }

    static void TestExec(string connectionString)
    {
      using (var conn = new SqlConnection(connectionString))
      {
        conn.Open();
        var cmd = new SqlCommand()
        {
          CommandText = "dbcc useroptions",
          Connection = conn
        };

        var reader = cmd.ExecuteReader();
        while (reader.Read())
        {
          if (reader.GetString(0) == "isolation level")
            Console.WriteLine(reader.GetString(1));
        }
      }
    }
  }

Expected behavior

A a new SqlConnection opened inside a TransactionScope must have the same isolation level defined in the TransactionScope.

Further technical details

Additional context The issue seems related to the connection pooling and the sp_reset_connection, because does not happen using Pooling=No in the connection string.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:2
  • Comments:9 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
asos-martinsmithcommented, Jun 18, 2023

What is the progress on the fix for this and why is it marked as “low priority”?

We have an application using Snapshot isolation.

It does a read and if a condition is met then does a write.

We are dependent on the documented behaviour

If a snapshot transaction attempts to commit modifications to data that has changed since the transaction began, the transaction will roll back and an error will be raised.

to give us an optimistic concurrency exception - this is broken for us because the isolation level gets reset before the write that would raise this exception so we don’t get the expected exception,

EDIT:

Though a workaround is to add an explicit

SET TRANSACTION ISOLATION LEVEL SNAPSHOT

in the relevant SQL

0reactions
tboloncommented, Aug 18, 2023

I am surprised this problem is still relevant.

See this code, which simply use a new TransactionScope() without any option:

using Microsoft.Data.SqlClient;
using System.Transactions;

class Program
{
  const string AzureConnectionString = "Server=tcp:xxx.database.windows.net,1433;Initial Catalog=xxx; Persist Security Info=False;User ID=xxx; Password=xxx;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout = 30;";
  const string SqlServerConnectionString = "Server=.\\SQLEXPRESS;Initial Catalog=xxx;Integrated Security=True;Encrypt=false";

  static void Main(string[] args)
  {
    Console.WriteLine("SQL SERVER");
    string connectionString = SqlServerConnectionString;
    using (var scope = new TransactionScope())
    {
      TestExec(connectionString); // Expected print: "serializable"
      TestExec(connectionString); // Expected print: "serializable"
    }

    Console.WriteLine("SQL AZURE");
    connectionString = AzureConnectionString;
    using (var scope = new TransactionScope())
    {
      TestExec(connectionString); // Expected print: "serializable"
      TestExec(connectionString); // Expected print: "serializable", Actual: "read committed snapshot"
    }
  }

  static void TestExec(string connectionString)
  {
    using (var conn = new SqlConnection(connectionString))
    {
      conn.Open();
      var cmd = new SqlCommand()
      {
        CommandText = "dbcc useroptions",
        Connection = conn
      };

      var reader = cmd.ExecuteReader();
      while (reader.Read())
      {
        if (reader.GetString(0) == "isolation level")
          Console.WriteLine(reader.GetString(1));
      }
    }
  }
}

And the output:

SQL SERVER
serializable
serializable
SQL AZURE
serializable
read committed snapshot

That means thay basically, the transaction scope transaction level is ignored when running more that one connection per TransactionScope (which is the recommended way to code : do not share a connection, instead open and close it when not needed, and expect the pooling will manage them.

I am surprise that all documentation still state that TransactionScope sets a Serializable scope by default and it’s never documented that the isolation level is not supported on Azure Sql Db (or buggy because it’s only ok for the first SqlConnection opened).

It does not seems that this bug is specific to Microsoft.Data.SqlClient (System.Data.SqlClient has the same behavior), and it should be redirected to the dotnet/runtime repo if TransactionScope or the documentation must be amended.

See also this:

Read more comments on GitHub >

github_iconTop Results From Across the Web

Is there some fundamental reason that TransactionScope ...
In the "On Premise" case the isolation level is not reset when the scope ends and a new transaction starts. This causes many...
Read more >
entity framework transactions and sql azure default ...
a) Yes. By default the isolation level will be Serializable. http://msdn.microsoft.com/en-us/library/ms172152(v=vs.90).aspx.
Read more >
Snapshot Isolation in SQL Server - ADO.NET
It opens a second connection and initiates a second transaction using the SNAPSHOT isolation level to read the data in the TestSnapshot table....
Read more >
SET TRANSACTION ISOLATION LEVEL (Transact-SQL)
The exception occurs when changing from any isolation level to SNAPSHOT isolation. Doing this causes the transaction to fail and roll back.
Read more >
Working with Transactions - EF6
In either case, the isolation level of the transaction is whatever isolation level the database provider considers its default setting.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found