Spring.Net Transaction events

by mandel on March 29th, 2010

Believe it or not during my working hours I write enterprise application for an energy company. Within the company I am one of the lucky guys that is currently developing a new application which uses a number of very cool libs and frameworks such as:

  • NHibernate using Fluent NHibernate for the mappings.
  • Spring.Net
  • NVelocity
  • RhinoMocks
  • etc..

One of the great things of using NHibernate and Spring.Net is that you can use the great transaction management provided by Spring.Net plus the great ORM in NHibernate. During the development the business decided that they wanted to be able to audit (I call it trace) the different operations that the users perform and also be able to configure the audit trails. Certainly this is the type of feature you have to carefully think about, the ideal trace we would like to have is:

  1. User log in
  2. User performs action
  3. Db Transaction
  4. Audit operations

It is very important that the audit is perform in an other transaction because we want to be able to isolate any possible error of the audit. If and error occurs while we audit, the business process should not be affected.

It took me a while to get the whole thing to work, and in order to help other people here is the code that will help you to perform something similar.

TransactionSynchronizer

This object will allow you to register a callback to be execute depending on the event you want to listen, that is, you can have a callback for a successful commit and for a roll back one. Here is the code:

Interface

using System;
 
namespace Com.Electrabel.Library.Transaction
{
	public interface ITransactionSynchronizer
	{
		void SetTransactionData(object data);
		void RegisterCommitCallback(Action<object> action);
		void RegisterRollbackCallback(Action<object> action);
	}
}

Implementation

using System;
using System.Collections.Generic;
using Spring.Transaction.Support;
 
namespace Com.Electrabel.Library.Transaction
{
	public class TransactionSynchronizer : ITransactionSynchronization, ITransactionSynchronizer
	{
		[ThreadStatic]
		private static IList<Action<object>> _commitCallbacks;
		[ThreadStatic]
		private static IList<Action<object>> _rollbackCallbacks;
 
		[ThreadStatic]
		private static object _transactionData;
 
		public void Suspend()
		{
		}
 
		public void Resume()
		{
		}
 
		public void BeforeCommit(bool readOnly)
		{
		}
 
		public void AfterCommit()
		{
		}
 
		public void BeforeCompletion()
		{
		}
 
		public void AfterCompletion(TransactionSynchronizationStatus status)
		{
			try
			{
				if (status == TransactionSynchronizationStatus.Committed && _commitCallbacks != null)
				{
					foreach (var action in _commitCallbacks)
					{
						action(_transactionData);
					}
				}
				else if (status == TransactionSynchronizationStatus.Rolledback && _rollbackCallbacks != null)
				{
					foreach (var action in _rollbackCallbacks)
					{
						action(_transactionData);
					}
				}
			}
			finally
			{
				if (_commitCallbacks != null)
					_commitCallbacks.Clear();
				if (_rollbackCallbacks != null)
					_rollbackCallbacks.Clear();
				_transactionData = null;
			}
		}
 
		public void SetTransactionData(object data)
		{
			_transactionData = data;
		}
 
		public void RegisterCommitCallback(Action<object> action)
		{
			TransactionSynchronizationManager.RegisterSynchronization(this);
			if (_commitCallbacks == null)
				_commitCallbacks = new List<Action<object>>(2);
 
			_commitCallbacks.Add(action);
		}
 
		public void RegisterRollbackCallback(Action<object> action)
		{
			TransactionSynchronizationManager.RegisterSynchronization(this);
			if (_rollbackCallbacks == null)
				_rollbackCallbacks = new List<Action<object>>(2);
			_rollbackCallbacks.Add(action);
		}
	}
}

Adding events

Registering a callback like that through code is easy, but what about Spring.Net? The following objects are helpers/wrapper that provide the events that can be used to hook the different audit operations using event handlers, that way we can easily configure everything through Spring.Net:

Interface

namespace Com.Electrabel.Vip.Services.Actions
{
	public interface IActionEventRaiser
	{
		event EventHandler Commit;
		event EventHandler Rollback;
 
		void SetContext(object context);
		void OnCommit(object data);
		void OnRollback(object data);
	}
}

Implementation

using System;
using Com.Electrabel.Library.Transaction;
using Com.Electrabel.Library.Validation;
using Com.Electrabel.Logging;
 
namespace Com.Electrabel.Vip.Services.Actions
{
	public class ActionEventRaiser : IActionEventRaiser
	{
		#region Variables
 
		private static readonly ILogEx _logger = (ILogEx) LogManager.GetLogger(typeof(ActionEventRaiser));
		private ITransactionSynchronizer _sync;
 
		#endregion
 
		#region Events
 
		public event EventHandler Commit;
		public event EventHandler Rollback;
 
		#endregion
 
		public void SetContext(object context)
		{
			_logger.DebugFormat("> SetContext({0})", context);
			_sync.RegisterCommitCallback(OnCommit);
			_sync.RegisterRollbackCallback(OnRollback);
			_sync.SetTransactionData(context);
			_logger.Debug("< SetContext()");
		}
 
		public void OnCommit(object data)
		{
			_logger.TraceFormat("> OnCommit({0})", data);
			if(Commit != null)
				Commit(this, new EventArgs());
			_logger.Trace("< OnCommit");
		}
 
		public void OnRollback(object data)
		{
			_logger.TraceFormat("> OnRollback({0})",data);
			if(Rollback != null)
				Rollback(this, new EventArgs());
			_logger.Trace("< OnRollback");
		}
 
		#region DI properties
 
		public ITransactionSynchronizer Synchronizer
		{
			get
			{
				return _sync;
			}
			set
			{
				ValidateArgs.Begin()
					.IsNotNull(value)
					.Check();
 
				// set the sync reference to keep track of it and register the different callbacks
				_sync = value;
				_logger.DebugFormat("OnCommit and OnRollbck registered to {0}", _sync);
			}
		}
		#endregion
	}
}

Using it

This is an example of a web service using the system. The idea is simple, in every transaction we set the context of the audit and we register the call back with the synchronizer, a piece of cake!

[Transaction]
public void Import(ImportData importData, PublicationData publicationData, IList<DataSeriesData> dataSeriesData)
{
	try
	{
		ValidateArgs.Begin()
			.IsNotNull(importData, "importData")
			.Check();
 
		ValidateRules.Begin()
			.IsUtcRange(importData.ImportRange)
			.Check();
 
		var import = CreateImport(importData);
 
		if (publicationData != null)
		{
			ValidateRules.Begin()
				.IsUtcRange(publicationData.ValidityPeriod)
				.Check();
 
			var publication = CreatePublication(publicationData);
			GetDataSeries(import, publication, dataSeriesData);
		}
 
		ImportRepository.Save(import);
		// in order to ensure that the different configured action contain the enough data to execute we will have to 
		// set it in the ActionEventRaiser which will take care of passing the data to the actions according to
		// the result of the transaction, that is Oncommit and OnRollback
		if (ImportEventRaiser != null)
		{
			ImportEventRaiser.SetContext(GetContext(import));
		}
	}
	catch (RulesValidationException ex)
	{
		_logger.Debug(ex.Message, ex);
		throw;
	}
	catch (ArgsValidationException ex)
	{
		_logger.Warn(ex.Message, ex);
		throw;
	}
	catch (Exception ex)
	{
		_logger.Error(ex.Message, ex);
		throw;
	}
}
#region DI properties
 
public IActionEventRaiser ImportEventRaiser { get; set; }
 
#endregion

I hope it helps!

Leave a Reply

You must be logged in to post a comment.