Spring.Net Transaction events
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:
- User log in
- User performs action
- Db Transaction
- 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!
