15 Dec 2010

Castle ActiveRecord / NHibernate Update Event Interceptor to Log Dirty Property Changes

Man I love the Castle Project documentation. Or at least, I would if I was a robot with a brain the size of a planet and an unlimited amount of time on my hands. Sadly I'm a human and find the whole thing impenetrable.

All I wanted to do was to intercept the NHibernate entity update event so that I could log the changes made. With kudos to this dude and this dude, here's how I did it:

1. In my project create a new class:

using Castle;
using Castle.ActiveRecord;
using Castle.ActiveRecord.Attributes;
using NHibernate;
using NHibernate.Event;

Note it uses a struct called EntityPropertyUpdate and a collection called RuntimeManager.EntityPropertyUpdates to keep a log of the changes. You'll have to roll your own if you want to do what I'm doing here, otherwise just modify the code as you see fit.

public class AuditUpdateListener : IPostUpdateEventListener
private const string _noValueString = "EMPTY";

private static string getStringValueFromStateArray(object[] stateArray, int position)
var value = stateArray[position];

return value == null || value.ToString() == string.Empty
? _noValueString
: value.ToString();

public void OnPostUpdate(PostUpdateEvent evt)
if (!(evt.Entity is Log_G))
var dirtyFieldIndexes = evt.Persister.FindDirty(evt.State, evt.OldState, evt.Entity, evt.Session);
foreach (var dirtyFieldIndex in dirtyFieldIndexes)
var oldValue = getStringValueFromStateArray(evt.OldState, dirtyFieldIndex);
var newValue = getStringValueFromStateArray(evt.State, dirtyFieldIndex);

if (oldValue != newValue)
RuntimeManager.EntityPropertyUpdates.Add(new RuntimeManager.EntityPropertyUpdate { EntityName = evt.Entity.GetType().Name, PropertyName = evt.Persister.PropertyNames[dirtyFieldIndex], PropertyFrom = oldValue, PropertyTo = newValue });

The other key thing is the ActiveRecordStarter.Initialize() call, which had to be changed to include the assembly containing the AuditUpdateListener class, which happened to be the main executing assembly. I was doing my AR Initialisation in global.asax so it now looks like this:

protected void Application_Start()
// ActiveRecord Setup
IConfigurationSource source = ActiveRecordSectionHandler.Instance;
Assembly asm = Assembly.Load("MyApp.DataLayer");
ActiveRecordStarter.Initialize(new Assembly[] {asm, Assembly.GetExecutingAssembly()}, source, new Type[] { });

That's all!

Incidentally though, if you want to use an ambient AR SessionScope like I do, add this to global.asax:

protected void Application_BeginRequest(Object sender, EventArgs e)
// Create a SessionScope for ActiveRecord
HttpContext.Current.Items.Add("ar.sessionscope", new SessionScope(FlushAction.Never));

protected void Application_EndRequest(object sender, EventArgs e)
// Close the SessionScope for ActiveRecord
SessionScope scope = HttpContext.Current.Items["ar.sessionscope"] as SessionScope;
if (scope != null)
catch (Exception ex)
HttpContext.Current.Trace.Warn("Error", "EndRequest: " + ex.Message, ex);

No comments:

Post a Comment

Comments are very welcome unless you're a spammer, in which case you should probably kill yourself.

If I helped you out today, you can buy me a beer below. Cheers!