Workflows and Transactions

QUESTION: How do I get access to a transaction inside my workflow?

ANSWER: First of all, this post has nothing to do with flowing a transaction into a workflow instance. That is a completely different topic which we might cover at some other time.

TransactionScopeActivity

With that out of the way, there are two ways to get access to a transaction inside of your workflow. The first is through the TransactionScopeActivity activity. In simplest terms, a System.Transactions.Transaction is created at the start of the TransactionScopeActivity and every activity inside the TransactionScopeActivity will have a valid Transaction.Current.

At the close of the TransactionScopeActivity the transaction is either completed or aborted. Consider the following psuedo-code:

using (TransactionScope scope = new TransactionScope())
{
DoSomething();
DoSomethingElse();
scope.Complete();
}

The equivalent workflow would look like:

<TransactionScopeActivity>
<DoSomething>
<DoSomethingElse>
</TransactionScopeActivity>

In both cases if an exception is thrown which crosses the scope's boundary, the transaction is aborted. In workflow terms this means that if the exception does not find a matching handler on an activity INSIDE the transaction scope then the transaction is aborted. 

Another similarity is that Transaction.Current is set on the thread in both cases. For the workflow it means that the DoSomething and DoSomethingElse activities will both find a transaction when executing any signal methods (Execute, Close, Cancel, HandleFault but NOT Initialize) as well as any handlers for status changes or queue item arrival.

Batching

While the rules of batching are enough to be a topic in their own right, we will touch on the beginnings of batching here. Stay tuned for future posts on this ...

Without getting into details, the following code will add a work item to the batch:

WorkflowEnvironment.WorkBatch.Add(somePendingWork, someWorkItem);

The somePendingWork object above must implement the IPendingWork interface and the someWorkItem is just a piece of data on which to do work; think of it the same way as you would the object state which you can optionally pass to ThreadPool.QueueUserWorkItem.

At the next commit point in the workflow, the runtime will call somePendingWork.Commit and pass a transaction and someWorkItem as parameters. Transaction.Current will also be set before the Commit method is called so that auto-enlisting calls will work correctly. Throwing an exception from the Commit method will cause the ENTIRE workflow persistence to fail and the exception will be thrown into the workflow for handling. If the Commit method returns without error this will be consider a successful vote for completion of the transaction. As long as everything in the batch succeeds, including persistence of the workflow itself, the transaction will be completed.

Once again, this is the simple version of how batching works. There are two other methods on IPendingWork as well as several rules about batch merging, batch compression, and batch clearing which will be covered in another post.

Conclusion

In conclusion, there are two ways to get a transaction in your workflow. You can model the transaction in your flow using the TransactionScopeActivity which works in a manner analogous to System.Transactions.TransactionScope. The other option is to delay the work until the next commit point and borrow the persistence transaction using batching.