Posts Tagged Transactional
Executing lambdas on transaction commit – or Transactional delegates.
Posted by Patrik Löwendahl in Code Design, design patterns on November 20, 2009
One great thing with the TransactionScope class is that it makes it possible to include multiple levels in your object hierarchy. Everything that is done from the point that the scope was created until it is disposed will be in the exact same transaction. This makes it easy to control business transactions from high up in the call stack and allow for everything to participate.
Everything but calls to delegates, until now. I checked in code in our project today to allow for this:
1: using(var scope = new TransactionScope)
2: {
3: repository.Save(order);
4: bus.SendMessage(new OrderStatusChanged {OrderId = order.Id, Status = order.Status});
5: scope.Complete();
6: }
7:
8: // In some message listener somwhere that listens to the Message sent
9: // But still in the transaction scope.
10:
11: public void HandleMessage(OrderStatusChanged message)
12: {
13: new OnTransactionComplete
14: (
15: () => publisher.NotifyClientsStatusChange(message.OrderId, message.NewStatus);
16: )
17: }
18:
19:
Why is this interesting?
Why would anyone want a delegate to be called on commit and only then? The scenarios where I find this and other similar scenarios (the code allows for OnTransactionRollback and AfterTransactionComplete as well) is where you have events or message sent to other subsystems asking them react. It can be more then one that listens and often you might not want to carry out the action if the transaction started high above in the business layer isn’t commited (like telling all clients that the status has changed and then the transaction rolls back).
How can I implement this?
The code to achieve this was fairly simple. Transactions in System.Transaction has a EnlistVoilatile method where you can send in anything that implements IEnlistmentNotification. This handy little interface defines four methods:
1:
2: public interface IEnlistmentNotification
3: {
4: void Prepare(PreparingEnlistment preparingEnlistment);
5: void Commit(Enlistment enlistment);
6: void Rollback(Enlistment enlistment);
7: void InDoubt(Enlistment enlistment);
8: }
Any object enlisting in a transaction with this interface will be told what’s happening and can participate in the voting. This particular implementation don’t vote it just reacts on Commit or Rollbacks.
The implementation spins around the class TransactionalAction that in it’s constructor accepts an Action delegate as a target and then enlists the object in the current transaction:
1: public TransactionalAction(Action action)
2: {
3: Action = action;
4: Enlist();
5: }
6:
7: public void Enlist()
8: {
9: var currentTransaction = Transaction.Current;
10: currentTransaction.EnlistVolatile(this, EnlistmentOptions.None);
11: }
Enlisting the object into the transaction will effectively add it to the transactions graph. As long as the transaction scope have a root reference, the object will stay alive. That is why this works by only stating new OnTransactionComplete( () => ) and you don’t have to save the reference.
The specific OnTransactionComplete, OnTransactionRollback and WhenTransactionInDoubt then inherits from TransactionalAction and overrides the appropriate method (the base commit ensures that we behave well in a transactional scenario):
1: public class OnTransactionCommit : TransactionalAction
2: {
3: public OnTransactionCommit(Action action)
4: : base(action)
5: { }
6:
7: public override void Commit(System.Transactions.Enlistment enlistment)
8: {
9: Action.Invoke();
10: base.Commit(enlistment);
11: }
12: }
Hey, what about AfterTransactionCommit?
The IEnlistmentNotification isn’t an interceptor model. It just allows you to participate in the transactional work. To be able to make After calls we need to use another mechanism. The TransactionCompleted event. As the previous code, this is fairly simple;
1: public class AfterTransactionComplete
2: {
3: private Action action;
4:
5: public AfterTransactionComplete(Action action)
6: {
7: this.action = action;
8: Enlist();
9: }
10:
11: private void Enlist()
12: {
13: var currentTransaction = Transaction.Current;
14:
15: if(NoTransactionInScope(currentTransaction))
16: throw new InvalidOperationException("No active transaction in scope");
17:
18: currentTransaction.TransactionCompleted += TransactionCompleted;
19: }
20:
21: private void TransactionCompleted(object sender, TransactionEventArgs e)
22: {
23: if(TransactionHasBeenCommited(e))
24: action.Invoke();
25: }
26: }
Summary
With some simple tricks using Transaction.Current and lambdas we can now participate in our transactions with our delegates. Download the code to get to do this:
1: new OnTransactionComplete
2: (
3: () => DoStuff()
4: )
5:
6: new OnTransactionRollback
7: (
8: () => DoStuff()
9: )
10:
11: new AfterTransactionCommit
12: (
13: () => DoStuff();
14: )
15:
16: // Also includes a more "Standard" api:
17: Call.OnCommit( () => DoStuff );
18:
19: // Since Call has extension methods, this is also valid:
20: ((Action)(()=> firstActionWasCalled=true)).OnTransactionCommit();
I hope this helps you build more robust solutions.
Code download (project is VS2010 but it works on .NET 2.0) [306kb]
2 Comments