Wrox Home  
Search
Professional .NET Framework 2.0
by Joe Duffy
April 2006, Paperback


Transactional Scopes

The first question that probably comes to mind when you think of using transactions is the programming model with which to declare the scope of a transaction over a specific block of code. And you'll probably wonder how to enlist specific resources into the transaction. In this section, we discuss the explicit transactions programming model. It's quite simple. This example shows a simple transactional block:

using (TransactionScope tx = new TransactionScope())
{
    // Work with transacted resources...
	tx.Complete();
}

With the System.Transactions programming model, manual enlistment is seldom necessary. Instead, transacted resources that participate with TMs will detect an ambient transaction (meaning, the current active transaction) and enlist automatically through the use of their own RM.

Once you've declared a transaction scope in your code, you, of course, need to know how commits and rollbacks are triggered. Before discussing mechanics, there are some basic concepts to understand. A transaction may contain multiple nested scopes. Each transaction has an abort bit, and each scope has two important state bits: consistent and done. These names are borrowed from COM+ transactions.

abort may be set to indicate that the transaction cannot commit (i.e., it must be rolled back).The consistent bit indicates that the effects of a scope are safe to be committed by the TM, and done indicates that the scope has completed its work. If a scope ends while the consistent bit is false, the abort bit gets automatically set to true and the entire transaction must be rolled back. This general process is depicted in Figure 3.

Figure 3
Figure 3: A simple transaction with two inner scopes.

In summary, if just one scope fails to set its consistent bit, the abort bit is set for the entire transaction, and the effects of all scopes inside of it are rolled back. Because of the poisoning effect of setting the abort bit, it is often referred to as the doomed bit. With that information in mind, the following sections will discuss how to go about constructing scopes and manipulating these bits.

An instance of the TransactionScope class is used to mark the duration of a transaction. Its public interface is extremely simple, offering just a set of constructors, a Dispose, and a Complete method. (An alternate programming model, called declarative transactions, which is not discussed here, facilitates interoperability with Enterprise Services.)

After a new transaction scope is constructed, any enlisted resource will participate with the enclosing transaction until the end of the scope. Constructing a new top-level scope installs an ambient transaction in Thread Local Storage (TLS), which can later be accessed programmatically through the Transaction.Current property. You saw a brief snippet of code above showing how to use these via the default constructor, the C# using statement (to automatically call Dispose), and an explicit call to Complete.

Calling Complete on the TransactionScope sets its consistent bit to true, indicating that the scope has successfully completed its last operation and is safe to commit. When Dispose gets called, it inspects consistent; if it is false, the transaction's abort bit is set. In simple cases with flat, single scope transactions, this is precisely when the effects of the commit or rollback are processed by the TM and its enlisted RMs. In addition to setting the various bits, it instructs the RMs to perform any necessary actions for commit or rollback. In nested scope scenarios, however, a child does not actually perform the commit or rollback; rather, the top-level scope is responsible for that (the first scope created inside a transaction).

Transactional Database Access Example (ADO.NET)

As a brief example of actually using transactions in your code, this C# snippet wraps a set of calls to a database inside a transaction. ADO.NET's SQL Server database provider automatically looks for an ambient transaction, instead of having to call CreateTransaction and associated methods on the connection manually, and avoids having to manually enlist its RM:

using (TransactionScope tx = new TransactionScope())
{
    IDbConnection cn = /*...*/;
    // ADO.NET detects the Transaction erected by the TransactionScope
	cn.Open();    
    // and uses it for the following commands automatically.
    IDbCommand cmd1 = cn.CreateCommand();
    cmd1.CommandText = "INSERT ...";
    cmd1.ExecuteNonQuery();    IDbCommand cmd2 = cn.CreateCommand();
    cmd2.CommandText = "UPDATE ...";
    // A call to Complete indicates that the ADO.NET Transaction is safe
	cmd2.ExecuteNonQuery();    
    // for commit. It doesn't actually complete until Dispose is called.
    tx.Complete();
}

Similar things were possible with version 1.x of the Framework, but of course it required a different programming model for each type of transacted resource you worked with. And it didn't automatically span transactions across multiple resource enlistments.

Wrapping Up

This article of course only touched on some of the capabilities of System.Transactions. We didn't talk about the various transaction creation options, deadlock avoidance, distributed transactions and two-phase commit, how to manually enlist RMs, how to build RMs, and a whole host of additional interesting parts of the new technology. But hopefully this quick overview is sufficient to get you familiar with the basic concepts, and ready to start exploring.

This article is adapted from Professional .NET Framework 2.0 by Joe Duffy (Wrox, 2006, ISBN: 0-7645-7135-4), from chapter 15 "Transactions." Joe is a Program Manager on the CLR Team at Microsoft, where he works on WinFX and the .NET Framework. Joe's other recent related articles at Wrox.com are Common Type System (CTS): One Platform to Rule Them All and CLR Method Internals.