System.Transactions integration with SQL Server
.NET Framework version 2.0 introduced a transaction framework that can be accessed through the System.Transactions namespace. This framework exposes transactions in a way that is fully integrated in .NET Framework, including ADO.NET.
In addition to the programmability enhancements, System.Transactions and ADO.NET can work together to coordinate optimizations when you work with transactions. A promotable transaction is a lightweight (local) transaction that can be automatically promoted to a fully distributed transaction on an as-needed basis.
Starting with ADO.NET 2.0, System.Data.SqlClient supports promotable transactions when you work with SQL Server. A promotable transaction does not invoke the added overhead of a distributed transaction unless the added overhead is required. Promotable transactions are automatic and require no intervention from the developer.
Promotable transactions are only available when you use the .NET Framework Data Provider for SQL Server (SqlClient
) with SQL Server.
Creating Promotable Transactions
The .NET Framework Provider for SQL Server provides support for promotable transactions, which are handled through the classes in the .NET Framework System.Transactions namespace. Promotable transactions optimize distributed transactions by deferring creating a distributed transaction until it is needed. If only one resource manager is required, no distributed transaction occurs.
Note
In a partially trusted scenario, the DistributedTransactionPermission is required when a transaction is promoted to a distributed transaction.
Promotable Transaction Scenarios
Distributed transactions typically consume significant system resources, being managed by Microsoft Distributed Transaction Coordinator (MS DTC), which integrates all the resource managers accessed in the transaction. A promotable transaction is a special form of a System.Transactions transaction that effectively delegates the work to a simple SQL Server transaction. System.Transactions, System.Data.SqlClient, and SQL Server coordinate the work involved in handling the transaction, promoting it to a full distributed transaction as needed.
The benefit of using promotable transactions is that when a connection is opened by using an active TransactionScope transaction, and no other connections are opened, the transaction commits as a lightweight transaction, instead of incurring the additional overhead of a full distributed transaction.
Connection String Keywords
The ConnectionString property supports a keyword, Enlist
, which indicates whether System.Data.SqlClient will detect transactional contexts and automatically enlist the connection in a distributed transaction. If Enlist=true
, the connection is automatically enlisted in the opening thread's current transaction context. If Enlist=false
, the SqlClient
connection does not interact with a distributed transaction. The default value for Enlist
is true. If Enlist
is not specified in the connection string, the connection is automatically enlisted in a distributed transaction if one is detected when the connection is opened.
The Transaction Binding
keywords in a SqlConnection connection string control the connection's association with an enlisted System.Transactions
transaction. It is also available through the TransactionBinding property of a SqlConnectionStringBuilder.
The following table describes the possible values.
Keyword | Description |
---|---|
Implicit Unbind | The default. The connection detaches from the transaction when it ends, switching back to autocommit mode. |
Explicit Unbind | The connection remains attached to the transaction until the transaction is closed. The connection will fail if the associated transaction is not active or does not match Current. |
Using TransactionScope
The TransactionScope class makes a code block transactional by implicitly enlisting connections in a distributed transaction. You must call the Complete method at the end of the TransactionScope block before leaving it. Leaving the block invokes the Dispose method. If an exception has been thrown that causes the code to leave scope, the transaction is considered aborted.
We recommend that you use a using
block to make sure that Dispose is called on the TransactionScope object when the using block is exited. Failure to commit or roll back pending transactions can significantly damage performance because the default time-out for the TransactionScope is one minute. If you do not use a using
statement, you must perform all work in a Try
block and explicitly call the Dispose method in the Finally
block.
If an exception occurs in the TransactionScope, the transaction is marked as inconsistent and is abandoned. It will be rolled back when the TransactionScope is disposed. If no exception occurs, participating transactions commit.
Note
The TransactionScope
class creates a transaction with a IsolationLevel of Serializable
by default. Depending on your application, you might want to consider lowering the isolation level to avoid high contention in your application.
Note
We recommend that you perform only updates, inserts, and deletes within distributed transactions because they consume significant database resources. Select statements may lock database resources unnecessarily, and in some scenarios, you may have to use transactions for selects. Any non-database work should be done outside the scope of the transaction, unless it involves other transacted resource managers. Although an exception in the scope of the transaction prevents the transaction from committing, the TransactionScope class has no provision for rolling back any changes your code has made outside the scope of the transaction itself. If you have to take some action when the transaction is rolled back, you must write your own implementation of the IEnlistmentNotification interface and explicitly enlist in the transaction.
Example
Working with System.Transactions requires that you have a reference to System.Transactions.dll.
The following function demonstrates how to create a promotable transaction against two different SQL Server instances, represented by two different SqlConnection objects, which are wrapped in a TransactionScope block. The code creates the TransactionScope block with a using
statement and opens the first connection, which automatically enlists it in the TransactionScope. The transaction is initially enlisted as a lightweight transaction, not a full distributed transaction. The second connection is enlisted in the TransactionScope only if the command in the first connection does not throw an exception. When the second connection is opened, the transaction is automatically promoted to a full distributed transaction. The Complete method is invoked, which commits the transaction only if no exceptions have been thrown. If an exception has been thrown at any point in the TransactionScope block, Complete
will not be called, and the distributed transaction will roll back when the TransactionScope is disposed at the end of its using
block.
// This function takes arguments for the 2 connection strings and commands in order
// to create a transaction involving two SQL Servers. It returns a value > 0 if the
// transaction committed, 0 if the transaction rolled back. To test this code, you can
// connect to two different databases on the same server by altering the connection string,
// or to another RDBMS such as Oracle by altering the code in the connection2 code block.
static public int CreateTransactionScope(
string connectString1, string connectString2,
string commandText1, string commandText2)
{
// Initialize the return value to zero and create a StringWriter to display results.
int returnValue = 0;
System.IO.StringWriter writer = new System.IO.StringWriter();
// Create the TransactionScope in which to execute the commands, guaranteeing
// that both commands will commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
try
{
// Opening the connection automatically enlists it in the
// TransactionScope as a lightweight transaction.
connection1.Open();
// Create the SqlCommand object and execute the first command.
SqlCommand command1 = new SqlCommand(commandText1, connection1);
returnValue = command1.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command1: {0}", returnValue);
// if you get here, this means that command1 succeeded. By nesting
// the using block for connection2 inside that of connection1, you
// conserve server and network resources by opening connection2
// only when there is a chance that the transaction can commit.
using (SqlConnection connection2 = new SqlConnection(connectString2))
try
{
// The transaction is promoted to a full distributed
// transaction when connection2 is opened.
connection2.Open();
// Execute the second command in the second database.
returnValue = 0;
SqlCommand command2 = new SqlCommand(commandText2, connection2);
returnValue = command2.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
}
catch (Exception ex)
{
// Display information that command2 failed.
writer.WriteLine("returnValue for command2: {0}", returnValue);
writer.WriteLine("Exception Message2: {0}", ex.Message);
}
}
catch (Exception ex)
{
// Display information that command1 failed.
writer.WriteLine("returnValue for command1: {0}", returnValue);
writer.WriteLine("Exception Message1: {0}", ex.Message);
}
}
// If an exception has been thrown, Complete will not
// be called and the transaction is rolled back.
scope.Complete();
}
// The returnValue is greater than 0 if the transaction committed.
if (returnValue > 0)
{
writer.WriteLine("Transaction was committed.");
}
else
{
// You could write additional business logic here, notify the caller by
// throwing a TransactionAbortedException, or log the failure.
writer.WriteLine("Transaction rolled back.");
}
// Display messages.
Console.WriteLine(writer.ToString());
return returnValue;
}
' This function takes arguments for the 2 connection strings and commands in order
' to create a transaction involving two SQL Servers. It returns a value > 0 if the
' transaction committed, 0 if the transaction rolled back. To test this code, you can
' connect to two different databases on the same server by altering the connection string,
' or to another RDBMS such as Oracle by altering the code in the connection2 code block.
Public Function CreateTransactionScope( _
ByVal connectString1 As String, ByVal connectString2 As String, _
ByVal commandText1 As String, ByVal commandText2 As String) As Integer
' Initialize the return value to zero and create a StringWriter to display results.
Dim returnValue As Integer = 0
Dim writer As System.IO.StringWriter = New System.IO.StringWriter
' Create the TransactionScope in which to execute the commands, guaranteeing
' that both commands will commit or roll back as a single unit of work.
Using scope As New TransactionScope()
Using connection1 As New SqlConnection(connectString1)
Try
' Opening the connection automatically enlists it in the
' TransactionScope as a lightweight transaction.
connection1.Open()
' Create the SqlCommand object and execute the first command.
Dim command1 As SqlCommand = New SqlCommand(commandText1, connection1)
returnValue = command1.ExecuteNonQuery()
writer.WriteLine("Rows to be affected by command1: {0}", returnValue)
' If you get here, this means that command1 succeeded. By nesting
' the Using block for connection2 inside that of connection1, you
' conserve server and network resources by opening connection2
' only when there is a chance that the transaction can commit.
Using connection2 As New SqlConnection(connectString2)
Try
' The transaction is promoted to a full distributed
' transaction when connection2 is opened.
connection2.Open()
' Execute the second command in the second database.
returnValue = 0
Dim command2 As SqlCommand = New SqlCommand(commandText2, connection2)
returnValue = command2.ExecuteNonQuery()
writer.WriteLine("Rows to be affected by command2: {0}", returnValue)
Catch ex As Exception
' Display information that command2 failed.
writer.WriteLine("returnValue for command2: {0}", returnValue)
writer.WriteLine("Exception Message2: {0}", ex.Message)
End Try
End Using
Catch ex As Exception
' Display information that command1 failed.
writer.WriteLine("returnValue for command1: {0}", returnValue)
writer.WriteLine("Exception Message1: {0}", ex.Message)
End Try
End Using
' If an exception has been thrown, Complete will
' not be called and the transaction is rolled back.
scope.Complete()
End Using
' The returnValue is greater than 0 if the transaction committed.
If returnValue > 0 Then
writer.WriteLine("Transaction was committed.")
Else
' You could write additional business logic here, notify the caller by
' throwing a TransactionAbortedException, or log the failure.
writer.WriteLine("Transaction rolled back.")
End If
' Display messages.
Console.WriteLine(writer.ToString())
Return returnValue
End Function