System.Transactions integration with SQL Server
Applies to: .NET Framework .NET .NET Standard
.NET includes 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, 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.
The Microsoft SqlClient Data Provider for SQL Server 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.
Creating promotable transactions
The Microsoft SqlClient Data Provider for SQL Server provides support for promotable transactions, which are handled through the classes in the 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, Microsoft.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 Microsoft.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 below 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.
Later, 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.
using System;
using System.Transactions;
using Microsoft.Data.SqlClient;
class Program
{
static void Main(string[] args)
{
string connectionString = "Data Source = localhost; Integrated Security = true; Initial Catalog = AdventureWorks";
string commandText1 = "INSERT INTO Production.ScrapReason(Name) VALUES('Wrong size')";
string commandText2 = "INSERT INTO Production.ScrapReason(Name) VALUES('Wrong color')";
int result = CreateTransactionScope(connectionString, connectionString, commandText1, commandText2);
Console.WriteLine("result = " + result);
}
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;
}
}