Using the New System.Transactions Namespace
The new System.Transactions namespace provides a single, unified means by which your code can take advantage of transactional resources, such as SQL Server databases and Microsoft Message Queues (MSMQ). In addition to providing a new way of consuming transactions as a client, the System.Transactions namespace also allows you to create your own resource manager, allowing other developers and code to take advantage of your proprietary resources in a transactional manner using the same client code that they would use for SQL, DTC, ADO.NET, or MSMQ transactions.
The new transaction manager automatically takes care of transaction escalation. Before System.Transactions, developers had to decide at design time if they needed their transactions to be local or distributed. The new transaction manager can automatically escalate local transactions to distributed transactions based on the resources being consumed and the resource managers involved in the transaction. What this means is that without having to change your code, your application can use low-cost local transactions during one execution, and then autoescalate to using distributed transactions the next time it is run, based on which resources the end user is accessing. The benefits of having a single set of transactional code to write that can be applied to any supported resource manager are too numerous to count.
The new transaction system provides two ways of making use of transactions as a consumer: explicitly using derivates of the transaction class, and implicitly using transaction scopes.
Using Explicit Transactions
When you explicitly create a transaction, you need to manually enlist resource managers in that transaction. In the example shown in Listing 18.5, the resource manager being enlisted is a SqlConnection instance. The CommittableTransaction hosts the Commit() and Rollback() methods. This makes it so that whether you're working with MSMQ, DTC, SQL, or any other resource manager, your transactional code can remain identical.
Listing 18.5. Using Explicit Transactions
The preceding sample is reusing the SampleDB database used in many chapters throughout this book. It's a simple database with a Customers table that contains a few columns and an ID column. When you run the preceding code, no changes to the table should be visible because the transaction was explicitly rolled back. If you change the Rollback() in the preceding code to Commit(), the transaction will complete and the changes made within the context of the transaction will be saved to the database, effectively deleting all customers with an ID greater than 3. Also note that you need to add a reference to the System.Transactions.dll assembly for the code in Listing 18.5 to work properly.
Using Implicit Transactions
Implicit transactions are much easier to manage by their very nature. As such, Microsoft actually recommends that you use implicit transactions for all of your transactional code unless you really need the explicit transactions shown in the preceding example. The main reason for this is that when using implicit transactions, transactions from multiple resource managers can coexist and operate with each other according to some predefined rules. For example, if you use implicit transactions, your code can simply automatically enlist in a parent transaction if one exists, or your code can create a new transaction if necessary. Even more useful is that if your code invokes other code that is also using implicit transactions, your implicit transaction may not commit until all nested transactions have voted to commit as well. This type of transaction nesting and the notion of submitting a vote on whether the transaction commits should be very familiar to anyone who has worked with COM+ before. In fact, System.Transactions TRansactions can actually interoperate with COM+ transactions, as you'll see in Chapter 40, "Using Enterprise Services."
Implicit transactions are accomplished through the use of the TRansactionScope class, which has a usage very similar to the using keyword. Code that resides within a transaction scope is inherently transactional, and access to any supported resource managers (such as SQL 2005) from within a transaction scope will use transactions implicitly. If your code doesn't get to the line where the Complete() method is called on a TRansactionScope, any transactions created within the scope will be rolled back, and any parent or ambient transactions utilized through nesting will receive a vote to roll back.
The code in Listing 18.6 illustrates the simple use of implicit transactions using transaction scopes.
Listing 18.6. Using Implicit Transactions
The preceding code tricks the compiler into allowing the programmer to create a divide-by-zero situation. Division by zero is never good and is guaranteed to throw an exception. As mentioned earlier, if your code can't execute the scope.Complete() line, the transaction won't be committed. If the scope created the transaction, it will be rolled back immediately. If the scope is part of a parent scope, it will vote to roll back the parent scope by virtue of its inability to complete. If you remove the four lines of code that create the divide-by-zero exception from Listing 18.6, the transaction will commit and the Customers table will be emptied.
Take a look at the code in Listing 18.7. It shows how you can nest transactionScope instances and have the Complete() method affect the parent scope. The main thing to remember is this: Every time a scope finishes without the Complete() method being called, a TRansactionAbortedException is thrown.
Listing 18.7. Using Nested Transaction Scopes
The preceding code creates a parent scope. Inside the scope, a loop is iterated seven times; each iteration attempts to delete a customer. The DeleteCustomer() method will properly delete all customers, except those with an ID of seven or greater. This simulates a potential problem with the database, showing that six out of seven loop iterations called the Complete() method on their respective transaction scopes. The seventh iteration fails to call the Complete() method, and this is the iteration that causes the transactionAbortedException to be thrown. The presence of this exception causes the parent transaction scope block to stopits Complete() method is never called.