Transaction Processing in ADO.NET: Using TransactionScope
Last Updated: Nov 10, 2025
6 min read
Legacy Archive
Legacy Guidance:This article preserves historical web development content. For modern .NET 8+ best practices, visit our Tutorials section.
Transaction Processing in ADO.NET: Using TransactionScope
ADO.NET 2.0 introduced robust features for database development, with transaction processing among the most important. Transactions protect data integrity and consistency across multiple operations—either every step commits, or the whole set rolls back.
Understanding Transactions
A transaction is a series of tasks that succeed or fail as a unit. You can implement transactions in stored procedures or in code using TransactionScope, which offers clear, concise syntax. Transactions may be local (single database) or distributed (multiple databases); TransactionScope can automatically promote a local transaction to a distributed one when needed.
Basic Transaction with TransactionScope
A minimal example that deletes discontinued products:
Simple Transaction (C#)
using System.Transactions;
using System.Data.SqlClient;
public void SimpleTransaction()
{
string connectionString = "Server=localhost;Database=MyDB;Integrated Security=true;";
using (TransactionScope transScope = new TransactionScope())
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
string query = "DELETE FROM Products WHERE Discontinued = 1";
SqlCommand cmd = new SqlCommand(query, conn);
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
// Complete the transaction
transScope.Complete();
}
}
}
Call Complete() to commit; otherwise the transaction rolls back automatically when the scope is disposed.
Improved Version with Error Handling
Add structured error handling for reliability:
Transaction with Error Handling (C#)
public void TransactionWithErrorHandling()
{
string connectionString = "Server=localhost;Database=MyDB;Integrated Security=true;";
using (TransactionScope transScope = new TransactionScope())
{
try
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open();
// First operation
SqlCommand cmd1 = new SqlCommand(
"UPDATE Products SET UnitPrice = UnitPrice * 1.1 WHERE CategoryID = 1",
conn);
int rowsAffected = cmd1.ExecuteNonQuery();
Console.WriteLine($"{rowsAffected} products updated");
// Second operation
SqlCommand cmd2 = new SqlCommand(
"INSERT INTO PriceHistory (ChangeDate, Description) VALUES (@Date, @Desc)",
conn);
cmd2.Parameters.AddWithValue("@Date", DateTime.Now);
cmd2.Parameters.AddWithValue("@Desc", "10% price increase for category 1");
cmd2.ExecuteNonQuery();
// If we got here, everything succeeded
transScope.Complete();
Console.WriteLine("Transaction committed successfully");
}
}
catch (SqlException ex)
{
Console.WriteLine($"Transaction failed: {ex.Message}");
// Automatic rollback on dispose
}
}
}
Distributed Transactions Across Multiple Databases
When two databases must be updated atomically, place both operations in the same scope; promotion to a distributed transaction happens automatically.
Distributed Transaction (C#)
public void DistributedTransaction()
{
string connString1 = "Server=Server1;Database=ProductsDB;Integrated Security=true;";
string connString2 = "Server=Server2;Database=InventoryDB;Integrated Security=true;";
using (TransactionScope transScope = new TransactionScope())
{
try
{
// First database operation
using (SqlConnection conn1 = new SqlConnection(connString1))
{
string query1 = "UPDATE Products SET Discontinued = 1 WHERE ProductID = @ID";
SqlCommand cmd1 = new SqlCommand(query1, conn1);
cmd1.Parameters.AddWithValue("@ID", 42);
conn1.Open();
cmd1.ExecuteNonQuery();
Console.WriteLine("Product marked as discontinued");
}
// Second database operation
using (SqlConnection conn2 = new SqlConnection(connString2))
{
string query2 = "DELETE FROM Inventory WHERE ProductID = @ID";
SqlCommand cmd2 = new SqlCommand(query2, conn2);
cmd2.Parameters.AddWithValue("@ID", 42);
conn2.Open();
cmd2.ExecuteNonQuery();
Console.WriteLine("Inventory record removed");
}
// Both operations succeeded
transScope.Complete();
Console.WriteLine("Distributed transaction completed successfully");
}
catch (SqlException ex)
{
Console.WriteLine($"Transaction failed and rolled back: {ex.Message}");
}
}
}
Real-World Example: Order Processing
A multi-step order workflow—create order, add items, update inventory—commits as a single unit.
Order Processing (C#)
public void ProcessOrder(int orderId, int customerId, List<OrderItem> items)
{
string connectionString = "Server=localhost;Database=SalesDB;Integrated Security=true;";
using (TransactionScope transScope = new TransactionScope())
{
try
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open();
// Create the order header
SqlCommand createOrder = new SqlCommand(
"INSERT INTO Orders (OrderID, CustomerID, OrderDate, Status) " +
"VALUES (@OrderID, @CustomerID, @OrderDate, @Status)",
conn);
createOrder.Parameters.AddWithValue("@OrderID", orderId);
createOrder.Parameters.AddWithValue("@CustomerID", customerId);
createOrder.Parameters.AddWithValue("@OrderDate", DateTime.Now);
createOrder.Parameters.AddWithValue("@Status", "Pending");
createOrder.ExecuteNonQuery();
// Add order items
foreach (var item in items)
{
SqlCommand addItem = new SqlCommand(
"INSERT INTO OrderDetails (OrderID, ProductID, Quantity, UnitPrice) " +
"VALUES (@OrderID, @ProductID, @Quantity, @UnitPrice)",
conn);
addItem.Parameters.AddWithValue("@OrderID", orderId);
addItem.Parameters.AddWithValue("@ProductID", item.ProductID);
addItem.Parameters.AddWithValue("@Quantity", item.Quantity);
addItem.Parameters.AddWithValue("@UnitPrice", item.UnitPrice);
addItem.ExecuteNonQuery();
// Update inventory
SqlCommand updateInventory = new SqlCommand(
"UPDATE Products SET UnitsInStock = UnitsInStock - @Quantity " +
"WHERE ProductID = @ProductID",
conn);
updateInventory.Parameters.AddWithValue("@Quantity", item.Quantity);
updateInventory.Parameters.AddWithValue("@ProductID", item.ProductID);
updateInventory.ExecuteNonQuery();
}
// All operations succeeded
transScope.Complete();
Console.WriteLine($"Order {orderId} processed successfully");
}
}
catch (SqlException ex)
{
Console.WriteLine($"Order processing failed: {ex.Message}");
// Automatic rollback
}
}
}
Configuring Transaction Options
Control isolation level and timeout via TransactionOptions and TransactionScopeOption.
Transaction Options (C#)
public void TransactionWithOptions()
{
string connectionString = "Server=localhost;Database=MyDB;Integrated Security=true;";
// Set transaction options
TransactionOptions options = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromSeconds(30)
};
using (TransactionScope transScope = new TransactionScope(
TransactionScopeOption.Required,
options))
{
try
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open();
SqlCommand cmd = new SqlCommand(
"UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 1",
conn);
cmd.ExecuteNonQuery();
cmd = new SqlCommand(
"UPDATE Accounts SET Balance = Balance + 100 WHERE AccountID = 2",
conn);
cmd.ExecuteNonQuery();
transScope.Complete();
}
}
catch (TransactionAbortedException ex)
{
Console.WriteLine($"Transaction aborted: {ex.Message}");
}
catch (SqlException ex)
{
Console.WriteLine($"Database error: {ex.Message}");
}
}
}
Nested Transactions
Compose operations with inner scopes; failing an inner scope can abort the outer scope as needed.
Nested Scopes (C#)
public void NestedTransactions()
{
using (TransactionScope outerScope = new TransactionScope())
{
try
{
// First operation
UpdateCustomerInfo();
// Nested transaction
using (TransactionScope innerScope = new TransactionScope())
{
try
{
UpdateOrderStatus();
innerScope.Complete();
}
catch (Exception ex)
{
Console.WriteLine($"Inner transaction failed: {ex.Message}");
throw; // Re-throw to fail outer transaction
}
}
// If we got here, everything succeeded
outerScope.Complete();
}
catch (Exception ex)
{
Console.WriteLine($"Outer transaction failed: {ex.Message}");
}
}
}
private void UpdateCustomerInfo()
{
// Implementation here
}
private void UpdateOrderStatus()
{
// Implementation here
}
Handling Long-Running Transactions
For lengthy operations, set a custom timeout on the scope to avoid unintended aborts.
Long-Running Transaction (C#)
public void LongRunningTransaction()
{
// Set a 5-minute timeout
TransactionOptions options = new TransactionOptions
{
Timeout = TimeSpan.FromMinutes(5)
};
using (TransactionScope transScope = new TransactionScope(
TransactionScopeOption.Required,
options))
{
try
{
// Perform long-running operations
ProcessLargeDataSet();
transScope.Complete();
}
catch (TimeoutException ex)
{
Console.WriteLine($"Transaction timed out: {ex.Message}");
}
}
}
private void ProcessLargeDataSet()
{
// Implementation for processing large amounts of data
}
Conclusion
TransactionScope simplifies transaction processing in ADO.NET, automatically handling local vs. distributed behavior, encouraging clean, scoped syntax, and ensuring fail-safe rollbacks. Whether targeting a single database or coordinating across many, it provides a reliable path to maintain data integrity.
Quick FAQ
What happens if I forget to call transScope.Complete() in my transaction?
If you don’t call Complete(), the transaction rolls back automatically when the scope is disposed. This ensures commits occur only on explicit success.
How does TransactionScope handle the promotion from local to distributed transactions?
Opening a second connection to a different database within the same scope promotes the transaction to a distributed one via DTC, transparently and without code changes.