Entity Framework and Transactionscope doesn't revert the isolation level after dispose of Transactionscope

前端 未结 1 997
萌比男神i
萌比男神i 2021-02-07 13:12

I am struggeling a bit with transaction scopes and entity framework.

Initially we want all our connections in the application to use snapshot isolation level when readin

1条回答
  •  臣服心动
    2021-02-07 13:36

    Well, after some digging today I found out a bit around this that I will share findings both for others to know and to get opinions and suggestions.

    There are several reasons why my issue happens dependent on the environment.

    Database server version:

    First of all, the result of the operations depends on the SQL Server version you are running (tested on SQL Server 2012 and SQL Server 2014).

    SQL Server 2012

    On SQL Server 2012, the last set isolation level will follow the connection on subsequent operations even if it is released back to the connection pool and retrieved back from other threads/actions. In practice; this means that if you in some thread/action sets the isolation level to read uncommitted using a transaction, the connection will preserve this until a another transaction scope sets it to a another isolation level (or by doing a SET TRANSACTION ISOLATION LEVEL command on the connection). Not good, you could suddenly get dirty reads without knowing it.

    For example:

    Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2).Select(mt => mt.LastUpdated).First());
    
    using (var scope = new TransactionScope(TransactionScopeOption.Required, 
                                            new TransactionOptions 
                                            { 
                                                IsolationLevel = IsolationLevel.ReadUncommitted 
                                            }))
    {
        Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2)
                                            .Select(mt => mt.LastUpdated).First());
        scope.Complete(); //tested both with and without
    }
    
    Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2).Select(mt => mt.LastUpdated).First());
    

    In this example, the first EF command will run with database default, the one within the transaction scope will run with ReadUncommitted, and the third one will also run with ReadUncommitted.

    SQL Server 2014

    On SQL Server 2014 on the other hand, each time a connection is acquired from the connection pool the sp_reset_connection procedure (it seems like it is this one anyway) will set the isolation level back to default on the database, EVEN if the connection is reacquired from within the same transaction scope. In practice; this means that if you have a transaction scope where you execute two subsequent commands only the first one will get the isolation level of the transaction scope. Also not good; you will get (based on the default isolation level on the database) either get a lock or snapshot readings.

    For example:

    Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2).Select(mt => mt.LastUpdated).First());
    
    using (var scope = new TransactionScope(TransactionScopeOption.Required, 
                                            new TransactionOptions 
                                            { 
                                                IsolationLevel = IsolationLevel.ReadUncommitted 
                                            }))
    {
        Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2)
                                 .Select(mt => mt.LastUpdated).First());
        Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2)
                                 .Select(mt => mt.LastUpdated).First());
        scope.Complete(); 
    }
    

    In this example, the first EF command will run with database default, the first one within the transaction will run with ReadUncommitted, but the second one within the scope will suddenly run as database default again.

    The manually open connection issue:

    There is other issues that happens on the different SQL Server versions with a manually open connection, however, we strictly don't need to do this so I am not going to dwell down into this problem now.

    Using Database.BeginTransaction:

    For some reason, the Database.BeginTransaction logic of Entity Framework seems to work in both databases which is OK, but in our code we works against two different databases and then we need transaction scopes.

    Conclusion:

    I find this handling of isolation level in conjunction with transaction scopes in SQL Server quite buggy after this, it is in my opinion not safe to use and could cause serious problems in any application as I see it. Be very cautious using this.

    But the fact remains, we need to have this working in our code. Having dealt with the tedious support at MS lately with not that great result I will first find a workaround that works for us. I will then report my findings using Connect and hope for the best that Microsoft does some actions around the transaction scope handling and connections.

    Solution:

    The solution (as far as I have come) is like this.

    Here is the requirements this solution will have: 1. The database MUST be READ COMMITTED in isolation level due to other applications that runs against the same database that requires this, we cannot use READ COMMITTED SNAPSHOT defaults on the database 2. Our application MUST have a default of SNAPSHOT isolation level - This is solved by using SET TRANSACTION ISOLATIONLEVEL SNAPSHOT 3. If there is a transaction scope, we need to honour the isolation level for this

    So based on these criterias the solution will be like this:

    In the context constructor, I register to the StateChange event where I in turn when the state is changed to Open and there is no active transaction defaults the isolation level to snapshot using classic ADO.NET. If a transaction scope is used, we need to honour the settings of this by running SET TRANSACTION ISOLATIONLEVEL based on the settings here (to limit our own code, we will only allow IsolationLevel of ReadCommitted, ReadUncommitted and Snapshot). As for transactions created by Database.BeginTransaction on the context it seems that this is honoured as it should so we don't do any special actions with these types of transactions.

    Here is the code in the context:

    public MyContext()
    {
        Database.Connection.StateChange += OnStateChange;
    }
    
    protected override void Dispose(bool disposing)
    {
        if(!_disposed)
        {
            Database.Connection.StateChange -= OnStateChange;
        }
    
        base.Dispose(disposing);
    }
    
    private void OnStateChange(object sender, StateChangeEventArgs args)
    {
        if (args.CurrentState == ConnectionState.Open && args.OriginalState != ConnectionState.Open)
        {
            using (var command = Database.Connection.CreateCommand())
            {
                if (Transaction.Current == null)
                {
                    command.CommandText = "SET TRANSACTION ISOLATION LEVEL SNAPSHOT";
                }
                else
                {
                    switch (Transaction.Current.IsolationLevel)
                    {
                        case IsolationLevel.ReadCommitted:
                            command.CommandText = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
                            break;
                        case IsolationLevel.ReadUncommitted:
                            command.CommandText = "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
                            break;
                        case IsolationLevel.Snapshot:
                            command.CommandText = "SET TRANSACTION ISOLATION LEVEL SNAPSHOT";
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                }
    
                command.ExecuteNonQuery();
            }
        }
    }
    

    I have tested this code both in SQL Server 2012 and 2014 and it seems to work. It is not the most nice code and it has it's limitations (e.g. it will for each EF execution always do a SET TRANSACTION ISOLATIONLEVEL against the database and thus add extra network traffic.)

    0 讨论(0)
提交回复
热议问题