Custom Transactions

By on 2/8/2010

If you haven't used TransactionScope from the System.Transactions namespace, you don't know what you're missing.  This system, introduced with .NET 2.0 provides a flexible mechanism for allowing your code to take part in transactions.  Many of the built-in subsystems such as ADO.NET automatically enlist in these transactions, but the real power comes from the fact that you can allow your own custom code to also take part in ambient transactions.

Recently at work, I wanted to perform a transaction across several unrelated modules with a custom resource. Of course TransactionScope was the first solution brought up. Unfortunately, after some analysis, I realized that this wasn't going to work for us; We have existing database code in those modules that have different conditions in which the transaction would be rolled back.

We needed a way to have multiple transaction scopes, each with different conditions of success or failure. I started thinking about how to accomplish this, and decided to write my own implementation which mimics the TransactionScope, but lets me control things a bit closer.  So I came up with a class which can be used like this:
using (Txn scope = Txn.New<MyScope>())
{
    // ... do work

scope.Commit(); }
Notice that the usage pattern is nearly identical to the TransactionScope API ... put the scope in a using statement, and call .Commit if the work was completed.  In the example above, the MyScope class is defined quite simply as:
public class MyScope : Txn
{
    protected override void OnStart()
    {
        Console.WriteLine("\tstarting");
    }

protected override void OnCommit() { Console.WriteLine("\tcommitting"); }

protected override void OnRollback() { Console.WriteLine("\trolling back"); } }
All you have to do is inherit from the Txn class, and implement three methods: OnStart, which occurs when the transaction is first beginning; OnCommit, which is invoked only when the top-most scope exits and all sub transactions were committed successfully; And OnRollback, which as you might imagine is only called if the transaction (or a subtransaction) was not committed successfully.

One difference between this API and the regular TransactionScope is that only one instance of "MyScope" will be created when the top-most transaction is first created. As I've alluded to, you can nest transactions just as you can with TransactionScope. And each scope must be committed if the entire transaction is to be completed.

The Txn class can be found below:
public abstract class Txn : IDisposable
{
    private Queue<bool> committed = new Queue<bool>();

public Txn() { }

[ThreadStatic] public static bool committable = true;

[ThreadStatic] public static int depth = 0;

[ThreadStatic] public static Txn current;

protected abstract void OnStart(); protected abstract void OnCommit(); protected abstract void OnRollback();

public void Commit() { this.committed.Enqueue(true); }

void IDisposable.Dispose() { if (committed.Count == 0 || !committed.Dequeue()) { committable = false; }

depth--; if (depth == 0) { if (committable) { this.OnCommit(); } else { this.OnRollback(); }

current = null; } }

public static T New<T>() where T : Txn, new() { depth++;

if (current == null) { current = new T(); }

if (depth == 1) { // first transaction, assume committable committable = true; current.OnStart(); }

return current as T; }

#region IDisposable Members

#endregion }
There is one caveat to mention. My requirement was to run this code in a windows service. The entire scope of the transaction would be single threaded, but there would be multiple ongoing transactions at once. To support this scenario, notice that some of the internal state of the Txn class uses the [ThreadStatic] attribute. This means that the API can be used from multiple threads at once and each thread would have its own state.

Of course, this might be a problem if you want to use this in an ASP.NET project. I've written about this issue before. There is probably a way to make this work using the techniques I outlined in that article, but I haven't given it a lot of thought (because I didn't need to). But I thought I'd share the work I did in case it is useful for anyone else.

See more in the archives