Why does SQL Server not respect the .Net isolation level? - c#

I have this code:
var to = new TransactionOptions();
to.IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted;
using (var ts = new TransactionScope(TransactionScopeOption.Required, to))
{
someQuery.ToList();
ts.Complete();
}
No matter what, the SQL Server Profiler shows that "someQuery" (and any other query on this transaction) run with an isolation level of "Read Committed".
The only way I can force to run as ReadUncommitted if, before someQuery.ToList();, I execute this line:
myContext.Database.ExecuteSqlCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;");
Why does SQL Server not respect the .Net isolation level? What can I do about this?

Related

Setback Isolation-Level with C# in SQL-Server 2016 after call [duplicate]

As demonstrated by previous Stack Overflow questions (TransactionScope and Connection Pooling and How does SqlConnection manage IsolationLevel?), the transaction isolation level leaks across pooled connections with SQL Server and ADO.NET (also System.Transactions and EF, because they build on top of ADO.NET).
This means, that the following dangerous sequence of events can happen in any application:
A request happens which requires an explicit transaction to ensure data consistency
Any other request comes in which does not use an explicit transaction because it is only doing uncritical reads. This request will now execute as serializable, potentially causing dangerous blocking and deadlocks
The question: What is the best way to prevent this scenario? Is it really required to use explicit transactions everywhere now?
Here is a self-contained repro. You will see that the third query will have inherited the Serializable level from the second query.
class Program
{
static void Main(string[] args)
{
RunTest(null);
RunTest(IsolationLevel.Serializable);
RunTest(null);
Console.ReadKey();
}
static void RunTest(IsolationLevel? isolationLevel)
{
using (var tran = isolationLevel == null ? null : new TransactionScope(0, new TransactionOptions() { IsolationLevel = isolationLevel.Value }))
using (var conn = new SqlConnection("Data Source=(local); Integrated Security=true; Initial Catalog=master;"))
{
conn.Open();
var cmd = new SqlCommand(#"
select
case transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'ReadUncommitted'
WHEN 2 THEN 'ReadCommitted'
WHEN 3 THEN 'RepeatableRead'
WHEN 4 THEN 'Serializable'
WHEN 5 THEN 'Snapshot'
end as lvl, ##SPID
from sys.dm_exec_sessions
where session_id = ##SPID", conn);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine("Isolation Level = " + reader.GetValue(0) + ", SPID = " + reader.GetValue(1));
}
}
if (tran != null) tran.Complete();
}
}
}
Output:
Isolation Level = ReadCommitted, SPID = 51
Isolation Level = Serializable, SPID = 51
Isolation Level = Serializable, SPID = 51 //leaked!
The connection pool calls sp_resetconnection before recycling a connection. Resetting the transaction isolation level is not in the list of things that sp_resetconnection does. That would explain why "serializable" leaks across pooled connections.
I guess you could start each query by making sure it's at the right isolation level:
if not exists (
select *
from sys.dm_exec_sessions
where session_id = ##SPID
and transaction_isolation_level = 2
)
set transaction isolation level read committed
Another option: connections with a different connection string do not share a connection pool. So if you use another connection string for the "serializable" queries, they won't share a pool with the "read committed" queries. An easy way to alter the connection string is to use a different login. You could also add a random option like Persist Security Info=False;.
Finally, you could make sure every "serializable" query resets the isolation level before it returns. If a "serializable" query fails to complete, you could clear the connection pool to force the tainted connection out of the pool:
SqlConnection.ClearPool(yourSqlConnection);
This is potentially expensive, but failing queries are rare, so you should not have to call ClearPool() often.
In SQL Server 2014 this seem to have been fixed. If using TDS protocol 7.3 or higher.
Running on SQL Server version 12.0.2000.8 the output is:
ReadCommitted
Serializable
ReadCommitted
Unfortunately this change is not mentioned in any documentation such as:
Behavior Changes to Database Engine Features in SQL Server 2014
Breaking Changes to Database Engine Features in SQL Server 2014
But the change has been documented on a Microsoft Forum.
Update 2017-03-08
Unfortunately this was later "unfixed" in SQL Server 2014 CU6 and SQL Server 2014 SP1 CU1 since it introduced a bug:
FIX: The transaction isolation level is reset incorrectly when the SQL Server connection is released in SQL Server 2014
"Assume that you use the TransactionScope class in SQL Server client-side source code, and you do not explicitly open the SQL Server connection in a transaction. When the SQL Server connection is released, the transaction isolation level is reset incorrectly."
Workaround
It appears that, since passing through a parameter makes the driver use sp_executesql, this forces a new scope, similar to a stored procedure. The scope is rolled back after the end of the batch.
Therefore, to avoid the leak, pass through a dummy parameter, as show below.
using (var conn = new SqlConnection(connString))
using (var comm = new SqlCommand(#"
SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = ##SPID
", conn))
{
conn.Open();
Console.WriteLine(comm.ExecuteScalar());
}
using (var conn = new SqlConnection(connString))
using (var comm = new SqlCommand(#"
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = ##SPID
", conn))
{
comm.Parameters.Add("#dummy", SqlDbType.Int).Value = 0; // see with and without
conn.Open();
Console.WriteLine(comm.ExecuteScalar());
}
using (var conn = new SqlConnection(connString))
using (var comm = new SqlCommand(#"
SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = ##SPID
", conn))
{
conn.Open();
Console.WriteLine(comm.ExecuteScalar());
}
For those using EF in .NET, you can fix this for your whole application by setting a different appname per isolation level (as also stated by #Andomar):
//prevent isolationlevel leaks
//https://stackoverflow.com/questions/9851415/sql-server-isolation-level-leaks-across-pooled-connections
public static DataContext CreateContext()
{
string isolationlevel = Transaction.Current?.IsolationLevel.ToString();
string connectionString = ConfigurationManager.ConnectionStrings["yourconnection"].ConnectionString;
connectionString = Regex.Replace(connectionString, "APP=([^;]+)", "App=$1-" + isolationlevel, RegexOptions.IgnoreCase);
return new DataContext(connectionString);
}
Strange this is still an issue 8 years later ...
I just asked a question on this topic and added a piece of C# code, which can help around this problem (meaning: change isolation level only for one transaction).
Change isolation level in individual ADO.NET transactions only
It is basically a class to be wrapped in an 'using' block, which queries the original isolation level before and restores it later.
It does, however, require two additional round trips to the DB to check and restore the default isolation level, and I am not absolutely sure that it will never leak the altered isolation level, although I see very little danger of that.

Unable to begin a distributed transaction using Entiy Framework

I'm having the following error executing this piece of code:
private bool _updateList(SysInfo _sysInfo, List<pList> _pList)
{
try
{
foreach (var p in _pList)
{
_context.spUpdatePListApprovalFlow(p.countryID, _sysInfo.User.JobRoleID, p.src, p.id, p.status, _sysInfo.User.Username);
}
return true;
}
catch (Exception ex) //debug only
{
throw; //throws error to the main try catch
}
}
ERROR
The operation could not be performed because OLE DB provider "MSDASQL"
for linked server "AS400_LINKEDSRV" was unable to begin a distributed
transaction.
However, everything works fine when I run the Stored Procedure in SQL Management Studio:
exec [dbo].[spUpdatePListApprovalFlow]
#CountryID = 123456,
#UserTypeID = 23456,
#Src = 1,
#Id = '123456789',
#Status = 30,
#Username = 'username'
I'm tired of digging for an answer nothing works... Few things I've tried:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
insert multiple transactions in the stored procedure
This sp has 2 sub stored procedures on it. One that writes into a table of the application's database, and another that updates a table in as400.
In EF6 stored procedures are called in an explicit transaction, by default. You can turn this off for a particular DbContext instance by changing its configuration after creating it, or for all instances of a DbContext type by changing it in the constructor. EG
using (var db = new Db())
{
db.Configuration.EnsureTransactionsForFunctionsAndCommands = false;
//. . .
}
See: https://msdn.microsoft.com/en-us/library/system.data.entity.infrastructure.dbcontextconfiguration.ensuretransactionsforfunctionsandcommands
Ok, after half a day trying to solve this I've finally solved it.
Resolution
Downgraded from Entity Framework 6 to Entity Framework 5 and the
distribuited transactions error has gone.
Just pay attention, if you're going to do this, you have to change some usings in your code. ( in auto generated code in the Data Model as well)
EF 6 uses
using System.Data.Entity.Core.Objects
EF 5 uses
using System.Data.Objects;
If you don't need distributed transactions you can try to disable them in the settings of the linked server:
EXEC master.dbo.sp_serveroption
#server=N'AS400_LINKEDSRV',
#optname=N'remote proc transaction promotion',
#optvalue=N'false'
Refer to this Microsoft page on Linked Servers.
Your System Administrator and/or DBA will probably need to make changes to address the missing linked server definition to your AS/400 server.
Another possible issue is that the AS/400 server (from IBM) lacks software support for the OLE DB data sources. This too would be something that the System Administration staff may need to address.

Calling a linked server fails after a snapshot connection has been disposed

We have a C# application using ADO.NET and an SQL-Server with SNAPSHOT Transaction Isolation Level. This is 'as is' and unfortunately cannot be modified.
Now we need insert stuff on a linked server.
We execute the following code (reduced to illustrate the problem):
// Create a snapshot Transaction and close the connection
using (var con = new SqlConnection(myConStr))
{
con.BeginTransaction(TransactionLevel.Snapshot);
}
// do something with a linked server
using (var con = new SqlConnection(myConStr))
{
using (var cmd = con.CreateCommand()
{
cmd.CommandText = "insert into LinkedServer.SomeDb..Table ...";
cmd.ExecuteNonQuery();
}
}
We get can an exception when trying to insert something into the linked server
'Remote access is not supported for transaction isolation level "SNAPSHOT"'
I wonder why it is not possible: We open the connection, make sure it is disposed (clearing all transactions, I guess) and use a second connection for the linked server call.
Executing the stuff in SSMS using plain SQL seems to work.
What are we missing? Is there a proper way to do it?
Thanks for any hints in the right direction.
The secret to understand the problem is the 'connection pooling' which is done by ADO.NET in the Background. The real connection is actually set to SNAPSHOT.
In the second part of the sample code, that connection simply gets reused thus still being in 'snapshot mode'.
The solution is to explicitly set the transaction isolation Level to something else right after opening the Connection.
using (var con = new SqlConnection(myConStr))
{
using (var cmd = con.CreateCommand()
{
cmd.CommandText = "set transaction isolation Level read committed";
cmd.ExecuteNonQuery();
}
using (var cmd = con.CreateCommand()
{
cmd.CommandText = "insert into LinkedServer.SomeDb..Table ...";
cmd.ExecuteNonQuery();
}
}

Transaction Scope - ORA-02089

I'm using Transaction Scope in my project to control transactions between several SQL instructions (insert, update, delete).
One of my instruction is call a existent stored procedure in database, but into this procedure there is a commit struction. I can't change it because it's used in others process.
When I execute, I get the error: ORA-02089: COMMIT is not allowed in a subordinate session
I'm declaring the transaction as the code bellow:
TransactionOptions options = new TransactionOptions();
options.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
using (TransactionScope transacao = new TransactionScope(TransactionScopeOption.Required, options))
{
....
transacao.complete()
}
What can I do to resolve that?

Is it possible to connect to SQL Server without specifying a database?

I'm trying to write some unit tests for my code that connect to SQL Server for persistence, but I would like to be able to run my unit tests by just pointing it at a SQL Server instance, and let the tests create their own database to run tests in, so after each test it can just drop the database and then on setup before the next test recreate it, so I know there is no legacy data or structures left over from a previous test effecting the next test.
In brief: no, you cannot do that. You might be able to leave out the database from the connection string, but in that case, that connection will be made to the configured default database of the login that's connecting to SQL Server (and that default database must exist at the time the connection is made)
If you want to have this scenario, you need to
first connect to your instance and database master and create your new testdb (or whatever it's called)
disconnect
in your tests, connect to the instance and the testdb database
Better yet: use a mocking framework of some sort so you don't even need an actual database in your testing scenario!
I use the following class to facilitate the OP's scenario:
public class MsSqlDatabaseCreator
{
public void Create(string connectionstring)
{
if (DatabaseExists(connectionstring))
{
DropDatabase(connectionstring);
}
CreateDatabase(connectionstring);
}
private static void CreateDatabase(string connectionString)
{
var sqlConnectionStringBuilder = new SqlConnectionStringBuilder(connectionString);
var databaseName = sqlConnectionStringBuilder.InitialCatalog;
sqlConnectionStringBuilder.InitialCatalog = "master";
using (var sqlConnection = new SqlConnection(sqlConnectionStringBuilder.ConnectionString))
{
sqlConnection.Open();
using (var sqlCommand = sqlConnection.CreateCommand())
{
sqlCommand.CommandText = $"CREATE DATABASE {databaseName}";
sqlCommand.ExecuteNonQuery();
}
}
}
private static bool DatabaseExists(string connectionString)
{
var sqlConnectionStringBuilder = new SqlConnectionStringBuilder(connectionString);
var databaseName = sqlConnectionStringBuilder.InitialCatalog;
sqlConnectionStringBuilder.InitialCatalog = "master";
using (var sqlConnection = new SqlConnection(sqlConnectionStringBuilder.ConnectionString))
{
sqlConnection.Open();
using (var command = sqlConnection.CreateCommand())
{
command.CommandText = $"SELECT db_id('{databaseName}')";
return command.ExecuteScalar() != DBNull.Value;
}
}
}
private static void DropDatabase(string connectionString)
{
var sqlConnectionStringBuilder = new SqlConnectionStringBuilder(connectionString);
var databaseName = sqlConnectionStringBuilder.InitialCatalog;
sqlConnectionStringBuilder.InitialCatalog = "master";
using (var sqlConnection = new SqlConnection(sqlConnectionStringBuilder.ConnectionString))
{
sqlConnection.Open();
using (var sqlCommand = sqlConnection.CreateCommand())
{
sqlCommand.CommandText = $#"
ALTER DATABASE {databaseName} SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE [{databaseName}]
";
sqlCommand.ExecuteNonQuery();
}
}
}
}
The important part is the switching of the database name (initial catalog) to master. This way you can have just one connectionstring.
What you want to accomplish is possible using a mocking framework, in which case you don't even have to "connect to a database", you simply mock the return values that the database should return in order for you to test your "db handler" implementation.
There are several to choose from when it comes to C#, I can recommend Rhino Mocks and Moq to name two. Here's a question detailing a bit more; https://stackoverflow.com/questions/37359/what-c-sharp-mocking-framework-to-use
Why not have the same named database dedicated for tests? and drop-create it every time. This way you won't need to mess about with connection strings - it is always the same.
And yet, there is a better solution: within all your tests, start transaction, do your test, where your data is messed up. Once you verified (or failed) the test, unroll the transaction. This way you don't need to drop-create your tests for every test, because the data is never changed.
But you'll need to make sure schema in test-database is always up to date. So you'll need to drop-create test database whenever your schema is changed.
I've blogged about database tests and how we deal with Entity Framework migrations. This might not be completely applicable to your situation, but might help with ideas.
Regarding using mocks in your tests - yes this is absolutely valid suggestion and should be followed most of the time. Unless you are trying to test the database layer. In that case no mocks will save you, and you just have to go to DB. Many times over I have tried to mock DbContext in EF, but never managed to simulate realistic DB behavior. So going to DB was easier for me, rather than simulating DB-mock.
I'd use SQL Server Management Objects for the task. It's Server and Database APIs doesn't necessarily need a connection string but I think you might still need to specify a database. You can use master for that. (Check jeroenh's answer about creating object using SMO API as well)
By the way, if you are using .Net 4.0.2 and up you can use LocalDB as well, which is even better.
Edit: Note that actually LocalDB is an SQL Server 2012 feature however you still need .Net Framework > 4.0.2 to be able to use it.

Categories

Resources