ADO.NET · Advanced

Transactions

A transaction makes several changes succeed or fail together. Without one, a crash halfway through a money transfer loses money.

Intermediate 5 min Lesson 18 of 32Examples use Microsoft.Data.SqlClient on .NET 9 with SQL Server 2022.
0/32 in ADO.NET
On this page · 5 min · Intermediate

In simple words#

Like a fixed-price thali: either you get the whole meal or you pay nothing. A transaction applies all changes or none.

BeginTransaction
Start the group of changes.
Commit / Rollback
Save all / undo all.
cmd.Transaction
Every command must join the transaction.

Learning objectives#

  • Use SqlTransaction with Commit and Rollback
  • Enlist every command in the transaction
  • Choose an isolation level
  • Use TransactionScope correctly with async code

Explanation#

connection.BeginTransaction() starts a transaction. Every command that should be part of it must have its Transaction property set.

Commit makes the changes permanent. If an exception occurs, call Rollback — or simply let the transaction be disposed without Commit, which rolls it back.

Isolation levels control what other sessions can see: ReadCommitted is the default; Snapshot avoids blocking readers; Serializable is the strictest.

TransactionScope is an ambient alternative. In async code create it with TransactionScopeAsyncFlowOption.Enabled, or it will not flow across await.

Code example: Place an order atomically#

C#OrderService.cs Illustrative
await using var conn = new SqlConnection(cs);
await conn.OpenAsync();
await using var tx = (SqlTransaction)await conn.BeginTransactionAsync();


try
{
    await using var stock = new SqlCommand(
        "UPDATE Products SET Stock = Stock - @qty WHERE Id = @id AND Stock >= @qty", conn, tx);
    stock.Parameters.Add("@qty", SqlDbType.Int).Value = 2;
    stock.Parameters.Add("@id", SqlDbType.Int).Value = 1;
    if (await stock.ExecuteNonQueryAsync() == 0)
        throw new InvalidOperationException("Not enough stock");


    await using var order = new SqlCommand(
        "INSERT INTO Orders (ProductId, Quantity) OUTPUT INSERTED.Id VALUES (@id, @qty)", conn, tx);
    order.Parameters.Add("@id", SqlDbType.Int).Value = 1;
    order.Parameters.Add("@qty", SqlDbType.Int).Value = 2;
    int orderId = (int)(await order.ExecuteScalarAsync())!;


    await tx.CommitAsync();
    Console.WriteLine(
quot;Order #
{orderId} committed");
} catch (Exception ex) { await tx.RollbackAsync(); Console.WriteLine(
quot;Rolled back:
{ex.Message}");
}
Expected output
Order #1002 committed   — or, if stock is too low: "Rolled back: Not enough stock"

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. The transaction is started on an open connection.
  2. Both commands receive the transaction through their constructor.
  3. If stock cannot be reduced, an exception aborts the whole operation.
  4. Commit saves both changes; Rollback undoes both.

Real-world use case#

Transferring money between accounts: the debit and the credit must both happen, or neither.

Common mistakes#

Forgetting to set cmd.Transaction

SqlClient throws “ExecuteReader requires the command to have a transaction when the connection assigned to the command is in a pending local transaction.”

Long transactions

Locks are held until Commit. Never wait for user input or call slow web services inside a transaction.

Best practices#

  • Keep transactions short
  • Pass the transaction to every command
  • Use await using so uncommitted transactions roll back
  • Consider Snapshot isolation for read-heavy systems

Remember this#

BeginTransaction → every command → Commit / Rollback.

Dispose without Commit = rollback.

Async TransactionScope needs AsyncFlowOption.Enabled.

Practice exercise#

Your task: Rewrite the order with TransactionScope for async code.

Show solution
C# Illustrative
using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
await using var conn = new SqlConnection(cs);
await conn.OpenAsync();          // enlists automatically
// … run the two commands without setting Transaction …
scope.Complete();                // commit when disposed

Mini quiz#

Transactions quizMultiple choice Question 1 of 2

What happens if a SqlTransaction is disposed without Commit?

Score so far: 0/0

Want more? Practise all ADO.NET MCQs · ADO.NET interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.