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.
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.
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.
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.
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.
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>
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.
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.
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>
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.