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:
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:using (Txn scope = Txn.New<MyScope>()) { // ... do work
scope.Commit(); }
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.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"); } }
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:
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.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 }
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.