In a recent MSDN TV interview, Mike Clark talked about (and demonstrated) the capabilities of the technology in the System.Transactions namespace, coming in .NET Framework 2.0. In other blog postings, such as in Angel Saenz-Badillos’ posting Whidbey ADO.NET System.Transactions Distributed Transactions and in Florin Lazar’s posting Transactions made easy: System.Transactions I found additional information on the topic. But all those publications have one thing in common: they look at the technology purely from the users’ point of view, not from the point of view of a participant in such a transaction.

Now, especially considering the availability of the new lightweight transaction manager (LTM), I was interested to find out what I’d have to do to be able to take part in a (potentially distributed) transaction with my own objects, as coordinated by System.Transactions. Before I go on describing my findings (and the things I haven’t yet quite understood), let me mention that I have no experience whatsoever with System.EnterpriseServices, so some misunderstandings may certainly be due to my ignorance.

A test object

To get things started, I thought I would need a simple object that would be able to act in the typical way required by any transaction: accept changes to data and commit or rollback those changes according to the transaction. So I wrote the following code for this purpose:

interface ITransactionable {
  void Commit( );
  void Rollback( );
}

class DataObject : ITransactionable {
  string oldDataField = String.Empty;
  string dataField = "preset string";

  public string DataField {
    get { return dataField; }
    set {
      if (oldDataField == String.Empty) oldDataField = dataField;
      dataField = value;
    }
  }

  public void Commit( ) {
    if (oldDataField != String.Empty) oldDataField = String.Empty;
  }

  public void Rollback( ) {
    if (oldDataField != String.Empty) {
      dataField = oldDataField;
      oldDataField = String.Empty;
    }
  }
}

Enlisting in a Transaction

To actually take part in a transaction, it’s necessary to Enlist in one, an object that implements one of three interfaces with slightly different purposes. In the context of a distributed transaction, the single object that enlists in one may often be a transaction itself. Think of the ADO.NET or SQL-Server transactions, which may be part of a distributed transaction that also involves other software modules or even completely different machines. What I did to be able to easily test this was to create a collection class typed for my ITransactionable interface above and implement the various System.Transactions interfaces on that class. So my collection, containing some objects, takes the role of a transaction in my own software system, which takes part in a larger overall transaction. The first thing I tried was to implement an interface called IPromotableSinglePhaseNotification, which is specifically made to work with the LTM, the big new thing in the whole namespace. This transaction manager is able to use a very lightweight architecture (hence the name) for the transaction handling as long as all participants are within one AppDomain, and it’s also able to promote the participants to full-fledged distributed transactions if it finds that at least one participant is not in the same AppDomain as the others. At least that’s what it should do… more about that later. My collection with the implementation of IPromotableSinglePhaseNotification looks like this:

class TransactionableCollection : List<ITransactionable>,
  IPromotableSinglePhaseNotification {
  public TransactionableCollection( ) {
    Enlist(Transaction.Current);
  }

  void Enlist(ITransaction transaction) {
    ILightweightTransaction lwtrans = transaction as ILightweightTransaction;
    if (lwtrans != null) lwtrans.PromotableSinglePhaseEnlist(this);
  }

  void IPromotableSinglePhaseNotification.Initialize( ) { }

  ITransaction IPromotableSinglePhaseNotification.Promote( ) { return null; }

  void IPromotableSinglePhaseNotification.Rollback(ISinglePhaseEnlistment
    singlePhaseEnlistment) {
    try {
      foreach (ITransactionable itransactionable in this)
        try {
          itransactionable.Rollback( );
        }
        catch { /* if necessary, handle the exception */ }
    }
    finally {
      singlePhaseEnlistment.Aborted( );
    }
  }

  void IPromotableSinglePhaseNotification.SinglePhaseCommit(ISinglePhaseEnlistment
    singlePhaseEnlistment) {
    try {
      foreach (ITransactionable itransactionable in this)
        itransactionable.Commit( );
      singlePhaseEnlistment.Committed( );
    }
    catch {
      // if necessary, handle the exception
      singlePhaseEnlistment.Aborted();
    }
  }
}

For the enlistment proper the collection uses an automatic mechanism similar to what SqlClient does, by looking at the current transaction and using that for its enlistment. It’s all just a sample, of course :-)

Now, the important thing is that this is all that’s really needed to take part in a transaction handled by the LTM. I used a small test program to see if things worked like expected:

DataObject ob1 = null;

using (TransactionScope scope = new TransactionScope( )) {
  TransactionableCollection coll = new TransactionableCollection( );
  ob1 = new DataObject( );
  coll.Add(ob1);
  Console.WriteLine("Unchanged object: {0}", ob1);
  ob1.DataField = "changed text";
  Console.WriteLine("Field has been changed: {0}", ob1);

  scope.Consistent = true; // try commenting this out
}

Console.WriteLine("Out of TransactionScope: {0}", ob1);

As expected, changes to the object are committed as long as the TransactionScope.Consistent flag is set to true before the scope is disposed. Changes are rolled back if the scope is not consistent when disposed. Fine up to this point!

The distributed transaction

You may have noticed that in the code up to this point, the method Promote would simply return null. The reason for that is simple: as long as there are only transaction participants from the same AppDomain, the LTM doesn’t see a need to promote a participant. The method isn’t called and nobody cares for the return value. So what will happen when I try to add another transaction participant that’s not in the same AppDomain? To find out, I inserted the following lines in my main test method, after the lines where I create and change my own object:

using (SqlConnection conn = new SqlConnection(connStr))
  conn.Open( );

The simple call to the Open method of the SqlConnection is enough to make the SqlClient enlist its transaction in the System.Transactions transaction, too, which in turn triggers the promotion of existing participants. What follows is a call to the Promote method of my IPromotableSinglePhaseNotification implementation. The problem is, I have no idea what exactly I should be supposed to do at this point. Returning null or returning the same transaction that we originally enlisted in renders various exceptions. I tried a lot of things at this point, without success… with the current documentation being what it is, I wasn’t able to find out what is expected of the Promote method. I’d be grateful if anybody could shed some light on this.

The other interesting thing is, if the lines of code where the Sql Server connection is opened occur before the line where my collection is created, there’s no problem at all! In this case, the Promote method is once again not called at all and everything works just fine, together with the Sql Server transaction. In the system Component Services management applet one can see at this point that the DTC actually has a registered transaction, in which we are now taking part.

The road less travelled

Because I was trying to adhere to the new concepts of the automatic promotion by the LTM, I hadn’t tried any of the more generic enlistment methods that are available in the “normal” ITransaction interface, as opposed to the ILightweightTransaction interface I was using. Implementing the IEnlistmentNotification interface, for instance, isn’t any more complicated at all. Here’s the code for that:

void IEnlistmentNotification.Commit(IEnlistment enlistment) {
  DoCommit( ); // same code as before
  enlistment.EnlistmentDone();
}

void IEnlistmentNotification.InDoubt( ) { }

void IEnlistmentNotification.Prepare(IPreparingEnlistment
  preparingEnlistment, byte[] recoveryInformation) {
  preparingEnlistment.Prepared();
}

void IEnlistmentNotification.Rollback(IEnlistment enlistment) {
  DoRollback( ); // same code as before
  enlistment.EnlistmentDone();
}

First, I didn’t notice any differences to the previous approach. Apparently, the LTM always tries to use the IPromotableSinglePhaseNotification interface as long as (a) it’s available and (b) the other participants, if any, are in the same AppDomain. So to really see a difference, it’s best to remove the implementation of that interface (or to not register with the PromotableSinglePhaseEnlist method). And then, suddenly, with only the IEnlistmentNotification interface implemented, things started to work! Suddenly I could combine my own code with the SqlConnection code in any way I wanted, without a hitch! Curious…

Confusion

Apart from that stuff with the promotion there’s another thing here that didn’t seem to make much sense. In that video on MSDN TV, Mike emphasized specifically the significance of the new lightweight model, where a DTC transaction would only be used if really necessary. Of course, the promotion model together with the IPromotableSinglePhaseNotification interface is an important part of that. But now I had things running with IEnlistmentNotification only, my test program behaved just the same as the demo program that Mike used in the video. Although I’m not using nor implementing the lightweight interfaces, a DTC transaction is only created when the Sql Server becomes part of the equation. So these are two things that may be worth writing about again later, when I find out more about them.

Download

I want to clean up my test code a bit before I make my sample available. Feel free to comment if you have any questions!