This past week I was in Sydney teaching our Industrial Strength .NET course. One of the topics that we covered yesterday was the various transaction control mechanisms available to .NET programmers and the patterns that you can use to implement them into your code. Our current IS.NET material is based on .NET 1.1 but one of the things that I can do as an instructor is splice in some .NET 2.0 material.

Following on from the discussion about “Services without Components”, I decided to quickly show how the classes and interfaces in the new System.Transactions namespace work together to provide a standard way to enlist in a distributed transaction in managed code.

TransactionScope

One of the central classes in the System.Transactions namespace from an application developers point of view is the TransactionScope class. The transaction scope class when created puts the code that follows within the scope of a logical transaction. The canonical example is creating a transaction scope and then going and doing something nasty to a database.

SystemTransactionsRollbackScenario

In this case I am connecting to the database and executing an unqualified delete command against one of the tables which would result in all the rows being removed. Fortunately because I am inside a transaction scope and I didn’t explicitly say that the transaction is complete it is assumed when I leave the scope of the outer using construct that a rollback should be performed. The TransactionScope class only exposes a Complete and Dispose methods (and a number of contructor overloads). If the Dispose method is called prior to the Complete method being called it is assumed that a call back

SystemTransactionsObjectModel1

If the dispose method is called prior to the Complete method being called it is assumed that all transactional operations performed within that scope should be rolled back. If I actually wanted to commit the transaction above I would have needed to execute the following code (don’t try this at home kids).

SystemTransactionsCommitScenario

One of the things that you will notice is that I did nothing to explicitly enlist this command in a transaction. Thats because ADO.NET is aware of the functionality provided by the System.Transactions namespace and somewhere in its implementation it enlists a resource manager to respond to notifications from the transaction coordinator.

Building Your Own Resource Manager

One of the great things about the new System.Transactions namespace is that you can write your code to interface with the transaction coordinator and in that way make your code aware of the transaction and be able to respond to commit and rollback operations.

Your code can determine whether it is inside the scope of a transaction by grabbing the static Current property of the Transaction class a determining whether it is null or not.

SystemTransactionsDetectingTransaction

The Current property on the transaction class holds a reference to a Transaction derived class which you can then use to enlist the resources under your control in the transaction. The Transaction class exposes a number of Enlist methods to support this and also exposes a traditional Rollback method.

SystemTransactionsObjectModel2

Lets look at what it takes to build your own resource manager. The example that I am going to use is a simple calculator with Add(…), Subtract(…), Multiply(…) and Divide(…) functions. In the code below I’ve using it both inside and outside the scope of a transaction.

SystemTransactionsBasicCalculatorUsage

If the calculator is indeed behaving transactionally the output of the WriteLine(…) call would be “2.0”, if not, it will be “5.0”. We’ll start with a calculator implementation which is not transactional.

SystemTransactionsBasicCalculator

In order to enlist in a transaction we need to write a resource manager. A resource manager is a piece of code which records what operations are performed on the resources that it manages and provides a mechanism for rolling back to the state prior to the transaction starting. From a System.Transactions point of view the only requirement for the resource manager is that it implements IEnlistmentNotification.

 SystemTransactionsObjectModel3

The problem is, we’ve got a bit of a chicken and egg situation here. The resource manager is kind of useless unless we have some kind of transaction logging mechanism to keep track of what we are doing, and the transaction, but we can’t really build the log unless we can talk to the resource manager to use it.

Unfortunately this means that I’m going to have to do a bit of an object model dump here and then try and explain it all in one go.

SystemTransactionsObjectModel5

In the model above we have a class called CalculatorContext. This is the resource manager for our transactional calculator (notice that it implements the IEnlistmentNotification interface). The context maintains the current state of the accumulator within the scope of the transaction and has a stack of commands that have been previously applied to that accumulator. The stack is fed by the ApplyCommand method which accepts any object which is derived from CalculatorCommand.

The CalculatorCommand class defines two abstract methods, Execute and Undo, both of which take an instance of the CalculatorContext. In order to hook into all of this infrastructure the Calculator class needs to get some treatment.

SystemTransactionsTransactionalCalculator

Notice how the methods now simply dispatch commands to the CalculatorContext instance “m_Context”, and how the Accumulator property just defers to the context to get the current value. The implementation of ApplyCommand is pretty straight forward, it first checks to see if we are running inside a transaction, and if thats the case whether its the first operation (by checking to see whether the command stack is initialised).

 SystemTransactionsApplyCommand

If its the first operation then it automatically enlists itself in the transaction, then pushes the command onto the command stack and executes it. On subsequent invocations in the same transaction it will just queue and execute commands. If there is no governing transaction then the command is just executed and no command history is maintained.

The only functionality left to implement on the resource manager is the actual Commit and Rollback implementations. Because we are keeping a rich history of what was done to the accumulator, all that needs to be done is that the stack needs to be cleared and set to null (in the case of a commit), or, in the case of a rollback the Undo methods need to be executed on each of the commands in history (then have the stack cleared and set to null).

SystemTransactionsCommitAndRollback

Executing the code results in the expected output. However, in the interests of full disclosure I need to tell you about an interesting side effect of my implementation.

SystemTransactionsOutput

If I had not run the code that was inside the scope of the transaction then the output would have been “2”, not “2.0”. This is because the decimal type only outputs the decimal point when a floating point operation has been performed. So in reality my implementation is flawed if you consider that to be relevant. The way around that would have been for each command to store the previous value of the accumulator and just restore that rather than reversing out their operations, and as a matter of fact you could implement a fairly tight rollback implementation with no commands at all – but I like this approach.

Dealing with Isolation Levels

The code above does not deal with varying isolation levels. If two threads were accessing this code then the results could be highly random, or interesting at best. The System.Transactions namespace does support the specification of isolation levels but that is beyond the scope of this post.

As a suggestion though for how you can handle isolation levels, you could maintain a dictionary of seperate transactions and their resource managers inside the Calculator class. This means that each thread would return a different accumulator value depending on what it was set to inside the transaction. Unfortunately, because there is only one real accumulator on the calculator it would be a case of “last-commit-wins” or locking on pretty much the first operation.

One approach for understanding whether something needs to be locked is having each transaction communicate with the other transactions to determine whether any of their logged commands overlap in their resource usages, but once again thats outside the scope of this post, and would be pretty meaningless in this simple calculator example.