Visitar URL original
SqlTransaction and TransactionScope leak isolation level · Issue #96 · dotnet/SqlClient · GitHub
Skip to content

SqlTransaction and TransactionScope leak isolation level #96

Closed

Description

@madelson

The TransactionScope class seems to "leak" it's isolation level to future queries on the same (pooled) connection.

I would expect the isolation level of the connection to be restored when the transaction ends or is disposed.

Here is a snippet which reproduces the behavior:

SqlConnection.ClearAllPools();
var conn = new SqlConnection(new SqlConnectionStringBuilder { DataSource = @".\sqlexpress", IntegratedSecurity = true }.ConnectionString);

Action printIsolationLevel = () =>
{
	var cmd = conn.CreateCommand();
	cmd.CommandText = @"SELECT CASE transaction_isolation_level 
						WHEN 0 THEN 'Unspecified' 
						WHEN 1 THEN 'ReadUncommitted' 
						WHEN 2 THEN 'ReadCommitted' 
						WHEN 3 THEN 'Repeatable' 
						WHEN 4 THEN 'Serializable' 
						WHEN 5 THEN 'Snapshot' END AS TRANSACTION_ISOLATION_LEVEL 
						FROM sys.dm_exec_sessions 
						where session_id = @@SPID";
	Console.WriteLine(cmd.ExecuteScalar());
};

conn.Open();
printIsolationLevel(); // "ReadCommitted"
conn.Close();

using (var scope = new TransactionScope(
					TransactionScopeOption.RequiresNew,
					new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.Serializable }))
{
	conn.Open();
	printIsolationLevel(); // "Serializable"
	conn.Close();
}

conn.Open();
printIsolationLevel(); // "Serializable" !!?
conn.Close();

Activity

  1. danmoseley commented on Apr 13, 2017

    @danmoseley

    TransactionsScope is in S.Transactions. Of course I don't know where the bug is..

  2. madelson commented on Apr 13, 2017

    @madelson
    ContributorAuthor

    Further investigations suggest that this may be a problem with SqlTransaction as well.

  3. jimcarley commented on Apr 13, 2017

    @jimcarley
  4. madelson commented on Apr 14, 2017

    @madelson
    ContributorAuthor

    The fact that the isolation level change goes on to pollute the connection pool makes the much more worrisome. However, even on a single connection we can see weird behavior where the transaction isolation level leaks out of the transaction scope:

    SqlConnection.ClearAllPools();
    var conn = new SqlConnection(new SqlConnectionStringBuilder { DataSource = @".\sqlexpress", IntegratedSecurity = true }.ConnectionString);
    
    Action<SqlTransaction> printIsolationLevel = (SqlTransaction transaction) =>
    {
    	var cmd = conn.CreateCommand();
    	cmd.Transaction = transaction;
    	cmd.CommandText = @"SELECT CASE transaction_isolation_level 
    						WHEN 0 THEN 'Unspecified' 
    						WHEN 1 THEN 'ReadUncommitted' 
    						WHEN 2 THEN 'ReadCommitted' 
    						WHEN 3 THEN 'Repeatable' 
    						WHEN 4 THEN 'Serializable' 
    						WHEN 5 THEN 'Snapshot' END AS TRANSACTION_ISOLATION_LEVEL 
    						FROM sys.dm_exec_sessions 
    						where session_id = @@SPID";
    	Console.WriteLine(cmd.ExecuteScalar());
    };
    
    conn.Open();
    
    printIsolationLevel(null); // ReadCommitted
    
    using (var transaction = conn.BeginTransaction(System.Data.IsolationLevel.Serializable))
    {
    	printIsolationLevel(transaction); // Serializable
    }
    
    printIsolationLevel(null); // Serializable
    
    conn.Close();
    
  5. changed the title [-]TransactionScope leaks isolation level[/-] [+]SqlTransaction and TransactionScope leak isolation level[/+] on Apr 14, 2017
  6. jimcarley commented on Apr 14, 2017

    @jimcarley

    @saurabh500 Including you because it looks like you were the last person to touch SqlConnection.

    This looks like a question for SqlConnection or SqlCommand.

  7. danmoseley commented on Apr 19, 2017

    @danmoseley
  8. saurabh500 commented on Apr 26, 2017

    @saurabh500
    Collaborator

    Right now SqlClient doesn't support TransactionScope in .Net Core .
    We are looking into SqlTransaction right now.

  9. geleems commented on May 1, 2017

    @geleems

    I performed some investigation, and realized this behavior is by design.
    Here is my test code:

    public static void Test()
    {
        string connString = "Server=tcp:<hostname>,1433;User ID=<id>;Password=<pw>;Connect Timeout=600;";
    
        SqlConnection.ClearAllPools();
        var conn = new SqlConnection(new SqlConnectionStringBuilder(connString).ConnectionString);
    
        Action<SqlTransaction> printIsolationLevel = (SqlTransaction transaction) =>
        {
            var cmd = conn.CreateCommand();
            cmd.Transaction = transaction;
            cmd.CommandText = @"SELECT CASE transaction_isolation_level 
                    WHEN 0 THEN 'Unspecified' 
                    WHEN 1 THEN 'ReadUncommitted' 
                    WHEN 2 THEN 'ReadCommitted' 
                    WHEN 3 THEN 'Repeatable' 
                    WHEN 4 THEN 'Serializable' 
                    WHEN 5 THEN 'Snapshot' END AS TRANSACTION_ISOLATION_LEVEL 
                    FROM sys.dm_exec_sessions 
                    where session_id = @@SPID";
            Console.WriteLine(cmd.ExecuteScalar());
        };
    
        conn.Open();
    
        printIsolationLevel(null); // ReadCommitted
    
        using (var transaction = conn.BeginTransaction(System.Data.IsolationLevel.Serializable))
        {
            printIsolationLevel(transaction); // Serializable
        }
    
        printIsolationLevel(null); // Serializable
    
        using (var transaction = conn.BeginTransaction(System.Data.IsolationLevel.ReadUncommitted))
        {
            printIsolationLevel(transaction); // ReadUncommitted
        }
    
        printIsolationLevel(null); // ReadUncommitted
    
        conn.Close();
    }
    

    Default isolation level is ReadCommitted, and it is replaced to new isolation level value when a transaction object is created. The isolation level value remains in connection of server side (client side does not store anything after transaction is disposed) even after transaction is finished because there is no need to reset it to default since isolation level is only consumed by transaction, and the value will be replaced to new value anyway when new transaction is started.

    This behavior is intended and described in TDS protocol design document (https://msdn.microsoft.com/en-us/library/dd339887.aspx). When a transaction object is disposed, it internally executes ROLLBACK, which sends TM_ROLLBACK_XACT request to server.

    Detail payload of the TM_ROLLBACK_XACT request is shown below:

     fBeginXact         =   BIT
      
     XACT_FLAGS         =   fBeginXact
                            7FRESERVEDBIT
      
     ISOLATION_LEVEL    =   BYTE
      
     XACT_NAME          =   B_VARBYTE
     BEGIN_XACT_NAME    =   B_VARBYTE
      
     RequestPayload     =   XACT_NAME
                            XACT_FLAGS
                            [ISOLATION_LEVEL
                            BEGIN_XACT_NAME]
    

    If fBeginXact is 1, then ISOLATION_LEVEL can specify the isolation level to use to start the new transaction, according to the definition at the end of this table. If fBeginXact is 0, then ISOLATION_LEVEL SHOULD NOT be present.

    fBeginXact bit is obviously set to 0 when transaction object is disposed, and RequestPayload does not have ISOLATION_LEVEL value intentionally.

    You can see this payload implementation at:
    https://github.com/dotnet/corefx/blob/master/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParser.cs#L6374

    ISOLATION_LEVEL
    Value: 0x00
    Description: No isolation level change requested. Use current.

    According to the spec document, ROLLBACK request does not change isolation level intentionally, and let it use current. So, this behavior is intended rather than a bug.

  10. madelson commented on May 2, 2017

    @madelson
    ContributorAuthor

    @geleems it isn't true that isolation level only affects transaction. As long as SQL is in autocommit mode (the default), each statement executes in an implicit transaction using the current isolation level. This is what caused me to report the bug.

    This is particularly problematic because of connection pooling, since the isolation levels can leak and affect any other part of the application.

  11. geleems commented on May 2, 2017

    @geleems

    @madelson

    since isolation level is only consumed by transaction

    I meant isolation level parameter is only consumed by method that creates transaction, which is SqlConnection.BeginTransaction(). Once isolation level is set to certain value, it will remain at that setting through the session as intended until you change it. If you want to just change the isolation level setting, and don't want transaction, you can do as following:

    SqlConnection.BeginTransaction(IsolationLevel.ReadCommitted).Dispose();
    

    However, I identified it is a problem that isolation level is not cleaned up when connection moves back to pool.

  12. madelson commented on May 2, 2017

    @madelson
    ContributorAuthor

    @geleems I agree that the behavior with pooling represents the biggest problem here; that's what burned us.

    Maybe it's a separate issue, but I do think that most callers to the API would expect the isolation level to reset on dispose despite the documentation that says otherwise. It seems weird that the recommended usage pattern would be to follow each transaction with another empty transaction just to get reasonable behavior. Much like the scoping of the foreach var (which changed in C#5), this seems like it could be one of those cases where despite the current behavior being intentional it is so confusing that it might be worth changing.

  13. madelson commented on Aug 8, 2017

    @madelson
    ContributorAuthor
  14. transferred this issue fromdotnet/corefxon May 16, 2019
  15. 22 remaining items

  16. moved this from Backlog to Investigating in SqlClient Boardon Apr 2, 2026
  17. moved this from Investigating to In progress in SqlClient Boardon Jun 1, 2026
  18. added a commit that references this issue on Jun 1, 2026
    9f67fb1
  19. added this to the 7.1.0-preview3 milestone on Aug 18, 2026
  20. priyankatiwari08 commented on Sep 17, 2026

    @priyankatiwari08
    Contributor

    /triage

  21. added a commit that references this issue on Oct 7, 2026
    e595dba
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Area\Connection PoolingUse this label to tag issues that apply to problems with connection pool.

Type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions