System.Transactions: Implement Your Own Resource Manager

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

One of the biggest paradigm shifts that has occurred with the introduction of .NET 2.0 probably is the introduction of System.Transactions. I had talked briefly about System.Transactions in my previous article about SQLCLR, but why should transactions be limited to databases? Don’t we need reliable code outside of databases as well? Sure we do! And that’s why you cannot afford to ignore System.Transactions and the tsunami of changes it will unleash in future generations of Microsoft platforms. Your first step is to learn how System.Transactions works.

How Does System.Transactions Work?

System.Transactions offers you an easy and straightforward way to implement your operations in a transactional manner. You can use it to implement a transaction in a number of ways, but a typical implementation is to wrap transactional operations inside a TransactionScope (the variable instance you use to make a block of code transactional) as follows:

using (TransactionScope ts = new TransactionScope())
{
   // Transactional code comes here.
   ts.Complete();
}

In the space commented, “Transactional code comes here,” you would use classes that hold or implement resource managers. Transactions work on resources, and each such resource is managed by a resource manager (RM). Resource managers work in cooperation with other entities, typically other processes or services called transaction managers (TMs).

A common way an RM can work in cooperation with a TM within a transaction is what is commonly referred to as a two-phase commit process. The following is the typical flow of a two-phase commit:

  1. An RM enlists in the transaction.
  2. The RM does its part, and signals the end of Phase 1 to the TM. This phase can also be referred to as the prepare phase.
  3. The TM gives the green signal to all RMs after they all have executed the prepare phase successfully.
  4. The RMs get the green signal and actually commit their work. In the event of a red signal, they roll back their work. This is the second phase, or the commit phase.
  5. In either case, the transaction coordinator coordinates with all the RMs to ensure that they all either succeed and do the requested work, or they all roll back their work together.

This entire process is provided by the System.Transactions framework, which leverages TMs such as MSDTC (Microsoft Distributed Transactions Coordinator). To use System.Transactions to manage resources participating in transactional operations, you need to either use an already existing RM or write your own.

Because the framework of System.Transactions is available but very few actual RMs are available right now, many developers likely will find it useful to write their very own RMs and use them in conjunction with the hordes of RMs that will appear in future releases of the .NET Framework or in third-party offerings. One rather common RM that does ship with .NET 2.0 is the SqlConnection class, which the example in this article will use in conjunction with a homegrown RM within a single transaction. But if you had to write your own RM, the first important question to ask is, “What does your RM do?”

What Does Your RM Do?

A critical question affects the general implementation of your RM: Does it manage a resource that is durable or volatile in nature?

In a typical two phase commit process, a durable resource refers to a resource that would require failure recovery. A good example would be transactional file copy. If you had to implement an RM that encapsulated the process of copying a file into a transaction, the file could actually be copied in Phase 1. In the event that the RM goes down, its recovery contract ensures that in the event of a rollback, the TM has enough information to restore the original state. A rollback in transactional file copy could be to delete the file or replace it with the pre-existing file. Enlisting durably in such a transaction is done by the System.Transaction.Transaction.EnlistDurable method.

Conversely, a volatile resource is one that doesn’t require recovery. A good example is in-memory data structures. A RM that encapsulates such an operation could enlist in the current running transaction in a volatile manner by using the System.Transaction.Transaction.EnlistVolatile method.

Yet another method of enlistment is PSPE, or Promotable Single Phase Enlistment, where a durable RM takes the ownership of a transaction, which can later be escalated to be managed by a TM as the conditions change. Because durable enlistment is more expensive than volatile enlistment, PSPE provides an attractive way of implementing an RM with durable resources, yet getting away with the performance penalties in many scenarios.

After choosing your enlistment method, you then need to implement the IEnlistmentNotification interface to ensure that you get the necessary callbacks from the TM to ensure proper commit or abort. Examples of TMs on the Windows platform are LTM (Lightweight Transaction Manager) for volatile enlistments and MSDTC for distributed or durable transactions.

Without any further delay, the following example demonstrates the implementation of these concepts.

Implementing an RM Using Volatile Enlistment

The following is an example of an RM that encapsulates the process of writing to a member variable within a transaction:

public class VolatileRM : IEnlistmentNotification
{
   private int memberValue = 0;
   private int oldMemberValue = 0;

   public int MemberValue
   {
      get { return memberValue; }
   }

   public void SetMemberValue(int newMemberValue)
   {
      Transaction currentTx = Transaction.Current;
      if (currentTx != null)
      {
         Console.WriteLine("VolatileRM: SetMemberValue -
                            EnlistVolatile");
         currentTx.EnlistVolatile(this, EnlistmentOptions.None);
      }
      oldMemberValue = memberValue;
      memberValue = newMemberValue;
   }

   #region IEnlistmentNotification Members

   public void Commit(Enlistment enlistment)
   {
      Console.WriteLine("VolatileRM: Commit");
      // Clear out oldMemberValue
      oldMemberValue = 0;
   }

   public void InDoubt(Enlistment enlistment)
   {
      Console.WriteLine("VolatileRM: InDoubt");
   }

   public void Prepare(PreparingEnlistment preparingEnlistment)
   {
      Console.WriteLine("VolatileRM: Prepare");
      preparingEnlistment.Prepared();
   }

   public void Rollback(Enlistment enlistment)
   {
      Console.WriteLine("VolatileRM: Rollback");
      // Restore previous state
      memberValue = oldMemberValue;
      oldMemberValue = 0;
   }

   #endregion
}

As you can see, the code checks whether it is currently within a transaction in SetMemberValue. If it indeed is, it enlists itself in the volatile transaction as follows and then performs the necessary operation while keeping within itself enough information for a possible rollback:

public void SetMemberValue(int newMemberValue)
{
   Transaction currentTx = Transaction.Current;
   if (currentTx != null)
   {
      Console.WriteLine("VolatileRM: SetMemberValue -
                          EnlistVolatile");
      currentTx.EnlistVolatile(this, EnlistmentOptions.None);
   }
   oldMemberValue = memberValue;
   memberValue = newMemberValue;
}

The code implements a bunch of other methods as well. Commit and Rollback, as their names suggest, either commit or roll back the work. The TM calls the Prepare method to notify your RM that it is requesting Phase 1 of the transaction to be committed, and to determine whether or not your RM is ready to do its work. At this point, you have one of three choices:

  1. Call the preparingEnlistment.Prepare() method. This notifies the TM that you are ready to go, and you are interested in listening to further notifications when you commit your work.
  2. Call preparingEnlistment.ForceRollBack() or specify preparingEnlistment.RecoveryInformation. In the event of re-enlistment, this provides the new instance of the RM with sufficient information to perform a graceful recovery. But, don’t worry about that just yet.
  3. Call preparingEnlistment.Done() to act as an innocent bystander. Merely observe the transaction and don’t participate in it. By calling Done, the TM skips notifying you about Phase 2 of the two-phase commit.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read