NHibernate Forge
The official new home for the NHibernate for .NET community

Creating an Audit Log using NHibernate Events

Wiki Page Hierarchy

Pages

Page Details

First published by:
bunceg
on 02-28-2009
Last revision by:
John Davidson
on 09-07-2011
6 people found this article useful.
Article
Comments (11)
History (34)
100% of people found this useful

Creating an Audit Log using NHibernate Events

Filed under: , , [Edit Tags]

Scenario

You want to create an audit table so that changes to business entities are tracked with a timestamp. You want this do be done automatically by NHibernate.

Options

There are a number of ways of doing this, using IInterceptor or the NHibernate 2.0 Event model. As the event model is fairly new, there isn't a lot of information or examples about how to use it therefore most examples deal with IInterceptor.The Audit logging itself can be recorded via multiple tables per class, single table to cover all classes, track all changes, track  latest changes etc.

 

Solution

This solution uses the Event model, a single table for all classes and track latest change only. It also depends on the database key being a GUID, not an integer. When a class is deleted, the audit information is also removed. This might not be appropriate for your scenario.

Background

The solution operates within a repository pattern (nielson, Domain Driven Design) design where there is a single repository wrapping the NHibernate Session. This repository exposes an Interface that is defined in the business layer that the NHibernate Repository implements. Therefore the repository depends on the domain, not the domain depends on the repository. An IoC product (Spring.NET, Unity etc.) is used to wire these together.

The Domain has no reference to anything to do with NHibernate but, to ease development; each business entity inherits from the abstract Entity class and implements the IEntity interface. It is the Entity class that tracks the audit information for the business class that inherits from it.

Entity Id's are GUID's as opposed to integers. The entity Id maps to the database primary domain.

Classes

The abstract Entity class is as follows

[Serializable]
public abstract class Entity : IEntity
{
private const string UnknownUser = "Unknown";

private Guid _id;
private byte[] _version;
private DateTime _createdTimestamp;
private string _createdBy;
private DateTime _updatedTimestamp;
private string _updatedBy;

[Snip: Property declarations removed for brevity]

public Entity()
{
_id = Guid.Empty;
_version = new byte[8];
_createdBy = string.IsNullOrEmpty(Thread.CurrentPrincipal.Identity.Name) ? UnknownUser : Thread.CurrentPrincipal.Identity.Name;
_createdTimestamp = DateTime.Now;
_entityName = this.GetType().FullName;
}
}

Each business class extends this however is appropriate.

Mappings

An example mapping file for a class that extends the entity class might be:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="Sample.Domain" assembly="Sample.Domain" default-access="field.camelcase-underscore" default-lazy="true">
<class name="Task" proxy="Sample.Domain.Interfaces.ITask, Sample.Domain.Interfaces" table="Task">
<id name="Id" column="Id" unsaved-value="00000000-0000-0000-0000-000000000000" access="property">
<generator class="guid.comb" />
</id>
<version name="Version" column="Version" type="binary" unsaved-value="null" generated="always"/>

<property name="Lookup" type="string" length="50" not-null="true" />
<property name="Description" type="string" length="255" not-null="true" />

<join table="Audit">
<key column="EntityId" />
<property name="EntityName" />
<property name="CreatedBy" />
<property name="CreatedTimestamp" />
<property name="UpdatedBy" />
<property name="UpdatedTimestamp"/>
</join>
</class>
</hibernate-mapping>

Implementation

From the mapping we can see that the Task class uses an Interface (ITask) for its proxy generation (though virtual methods are just as acceptable) but more importantly that the Task class is populated from a join between the Audit table and the Task table. The join column is the Entity ID which, like the Id in the task table is a GUID. As GUID's are almost guaranteed to be unique there is very little likelihood of the being a key collision between the Task table and (e.g.) a Group table each storing their ID in the same column. Unfortunatley this is not the case for a HiLo key generation mechanism.

Therefore this Join element is telling NHibernate to do a SQL join on the Id of the primary table (Task) with the Entity ID of the Audit table. In effect, its the same as multi table inheritence but without the inheritence.

The power of this is that Nhibernate automatically keeps the two tables in line... an add to Task results in two SQL inserts (Task and Audit), a delete of Task results in two deletes etc. If you wish to retain audit information, but not the business data, in the case of a delete then this solution is not for you, though you could consider the "pre-delete" event to deal with retaining the audit information somehow.

Wiring into the NHibernate Event model

The NHibernate 2.0 Event model can be implemented a number of ways, inherit from a base class or implement an interface. This solution uses an interface as it allows a single listener class to deal with both updates and inserts.

The best events to use for Auditing in this scenario are "pre-update" and "pre-insert". First we write a listener class that implements two interfaces IPreUpdateEventListener and IPreInsertEventListener. This class needs to ensure that the "state" of the object to be written is updated prior to NHibernate writing out the class information. Unfortunately this state information is held in a string array and cannot be accessed in a TypeSafe manner.

internal class AuditEventListener : IPreUpdateEventListener, IPreInsertEventListener
{
public bool OnPreUpdate(PreUpdateEvent e)
{
UpdateAuditTrail(e.State, e.Persister.PropertyNames, (IEntity)e.Entity);
return false;
}
public bool OnPreInsert(PreInsertEvent e)
{
UpdateAuditTrail(e.State, e.Persister.PropertyNames, (IEntity)e.Entity);
return false;
}
private void UpdateAuditTrail(object[] state, string[] names, IEntity entity)
{
var idx = Array.FindIndex(names, n => n == "UpdatedBy");
state[idx] = string.IsNullOrEmpty(Thread.CurrentPrincipal.Identity.Name) ? "Unknown" : Thread.CurrentPrincipal.Identity.Name;
entity.UpdatedBy = state[idx].ToString();
idx = Array.FindIndex(names, n => n == "UpdatedTimestamp");
DateTime now = DateTime.Now;
state[idx] = now;
entity.UpdatedTimestamp = now;
}
}

Don't ask me why we return false in the implemented methods.... I'm not sure yet, but it works :)

So now when an object is written (the presumption is that all objects written implement the business IEntity interface) Nhibernate will run the listener, find the audit properties in the current NHibernate State and update them.To ensure the object itself also has the latest values, we update the entity instance as well.

Hooking NHibernate into the Listener

This is the easy bit - add the following to the hibernate.cfg.xml file (within the SessionFactory element) and you're done!

<event type="pre-update">
<listener class="Sample.Repository.NHibernate.AuditEventListener, Sample.Repository.NHibernate" />
</event>
<event type="pre-insert">
<listener class="Sample.Repository.NHibernate.AuditEventListener, Sample.Repository.NHibernate" />
</event>

A Side Note

This article works great, thanks Graham.  I just wanted to point out there is an NHibernate gotcha/ feature related to using the OnPre* events with inheritance which is documented further on this WIKI post.

Recent Comments

By: ice machines Posted on 11-21-2011 3:36

Hi! I've been following your blog for a long time now and finally got the courage to go ahead and give you a shout out from Atascocita Texas! Just wanted to tell you keep up the good job!http://www.cbfi-icemachine.com

By: ice machines Posted on 11-18-2011 5:19

Great website you have here but I was wondering if you knew of any discussion boards that cover the same topics discussed in this article? I'd really like to be a part of online community where I can get comments from other knowledgeable individuals that share the same interest. If you have any recommendations, please let me know. Many thanks!www.bestgarmentaccessories.com

By: sarah5322 Posted on 10-29-2011 23:41

Making income online can be troublesome at the start however Its the people that be persistant to it that always succeed.

www.housesforsaleincenturion.co.za

www.furnitureremovals.org.za

www.howcanigettallerguide.com

www.getsmokelesscigarettes.com

www.bestwaytoloseweightguide.net

By: David Simpson Posted on 10-22-2011 15:31

it’s really worth while doing one step at a time than multi-tasking move setting up a site. I’ve also exprience that kind of delemma when so many ideas coming to your mind and having hard time to decide which is which, and taking for granted the basics.

www.ecowatersandiego.com

View All
Powered by Community Server (Commercial Edition), by Telligent Systems