Azure SQL Database: Handling Transactions

Background

This article includes some of the aspects of transaction handling in Windows Azure SQL Database. 

Some of the information in this article is superseded by the announcement of Elastic Database Transactions. In sum, transactions across Azure SQL Databases are supported using .NET 4.6.1. For more information, see Elastic Database Transactions with Azure SQL Database.

Note
If you wish to provide feedback for this documentation please either send e-mail to azuredocs@microsoft.com or use the Comment field at the bottom of this page (sign-in required).

Local Transactions

SQL Database supports local transactions. These types of transactions are done with the Transact-SQL commands BEGIN TRANSACTION, ROLLBACK TRANSACTION, COMMIT TRANSACTION. They work exactly the same as they do on SQL Server.

Isolation Level

SQL Database default database wide setting is to enable read committed snapshot isolation (RCSI) by having both the READ_COMMITTED_SNAPSHOT and ALLOW_SNAPSHOT_ISOLATION database options set to ON, learn more about isolation levels here. You cannot change the database default isolation level. However, you can control the isolation level explicitly on a connection. On way to do this you can use any one of these in SQL Database before you BEGIN TRANSACTION:

SET TRANSACTION  ISOLATION LEVEL  SERIALIZABLE
SET TRANSACTION  ISOLATION LEVEL  SNAPSHOT
SET TRANSACTION  ISOLATION LEVEL  REPEATABLE READ
SET TRANSACTION  ISOLATION LEVEL  READ COMMITTED
SET TRANSACTION  ISOLATION LEVEL  READ UNCOMMITTED

SET TRANSACTON ISOLATION LEVEL controls the locking and row versioning behavior of Transact-SQL statements issued by a connection to SQL Server and spans batches (GO statement). All of the above works exactly the same as SQL Server.

Note: The above statement "SET TRANSACTION ISOLATION LEVEL READ COMMITTED" will set the isolation level to read committed snapshot isolation (RCSI). This isolation level is different from read committed (RC). In other words, the default behavior of “SET TRANSACTION ISOLATION LEVEL READ COMMITTED" in on-premise SQL Server is RC, but in SQL Database is RCSI. If you want to use exact RC (not RCSI) behavior in SQL Database, you have to set the lock hint to the SQL statement.

Distributed Transactions in SQL Database

SQL Database does not support distributed transactions, which are transactions that multiple transaction managers (multiple resources). For more information, see Distributed Transactions (ADO.NET). This means that SQL Database doesn’t allow Microsoft Distributed Transaction Coordinator (MS DTC) to delegate distributed transaction handling. Because of this you can’t use ADO.NET or MSDTC to commit or rollback a single transaction that spans across multiple SQL Databases or a combination of SQL Database and an on-premise SQL Server.

This doesn’t mean that SQL Database doesn’t support transactions, it does. However, it only supports transactions that are not escalated to a resource manager such as MS DTC. An article entitled: Transaction Management Escalation on MSDN can give you more information.

TransactionScope

The TransactionScope class provides a simple way to mark a block of code as participating in a transaction, without requiring you to interact with the transaction itself. The TransactionScope class works with the Transaction Manager to determine how the transaction will be handled. If the transaction manager determines that the transaction should be escalated to a distributed transaction, using the TransactionScope class will cause a runtime exception when running commands against SQL Database, since distributed transactions are not supported.

So the question is when is it safe to use the TransactionScope class with SQL Database? The simple answer is whenever you use it in a way that the Transaction Manager does not promote the transaction to a distributed transaction. So another way to ask the question is what causes the transaction manager to promote the transaction? Here are some cases that cause the transaction to be promoted:

  • When you have multiple connections to different databases.
  • When you have nested connections to the same database.
  • When the ambient transaction is a distributed transaction, and you don’t declare a TransactionScopeOption.RequiresNew.
  • When you invoke another resource manager with a database connection.

Juval Lowy wrote an excellent whitepaper (downloadable here) all about System.Transactions, where he covers promotion rules in detail.

Because transaction promotion happens at runtime you need to make sure you understand all your runtime code paths in order to use TransactionScope successfully. You don’t want the thread calling your method to be involved in an ambient transaction.

SqlTransaction

One way to write your code without using the TransactionScope class is to use SqlTransaction. The SqlTransaction class doesn’t use the transaction manager, it wraps the commands within a local transaction that is committed when you call the Commit() method. You still can’t have a single transaction across multiple databases; however SqlTransaction class provides a clean way in C# to wrap the commands. If your code throws an exception, the using statement guarantees a call to IDispose which rolls back the transaction.

Here is some example code to look over:

using (SqlConnection sqlConnection =
 new SqlConnection(ConnectionString))
{
 sqlConnection.Open();
 
 using (SqlTransaction sqlTransaction =
 sqlConnection.BeginTransaction())
 {
 // Createthe SqlCommand object and execute the first command.
 SqlCommand sqlCommand = new  SqlCommand("sp_DoFirstPieceOfWork",
 sqlConnection, sqlTransaction);
 
 sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
 
 sqlCommand.ExecuteNonQuery();
 // Createthe SqlCommand object and execute the first command.
 SqlCommand sqlCommand = new  SqlCommand("sp_DoSecondPieceOfWork",
 sqlConnection, sqlTransaction);
 
 sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
 
 sqlCommand.ExecuteNonQuery();
 
 sqlTransaction.Commit();
 }
}