We have learned that entity state in the memory is synchronized to the database either when session is flushed or transaction is committed. All the calls we made to ISession.Save
or ISession.Update
till that point do not send any changes to the database. Instead, what they do is create an event and push it into the events array maintained by the session. When the session is flushed (explicitly or via committing the transaction), then NHibernate goes through each of the events and executes them one by one. As usual, I am making it easy by giving you a 10,000 feet view of how events work. Exact working of the events is more complex in reality. Intent of this section is not to understand how the events work internally. This section is more about how to listen to NHibernate's internal events and how to be notified so that we can execute our own code in response to a particular action that NHibernate carries out internally. Event listener is the mechanism that we would use for this purpose.
Event listener is the mechanism that lets us hook into the eventing system of NHibernate and execute our custom logic at various times during the execution of events. The way event listeners work is very simple. You build an event listener by implementing one of the several event listener interfaces and then hooking your implementation into the configuration object via appropriate event handler. There are several event listeners available to implement. Following table lists the event listeners that can be useful in most situations:
Event listener name |
When is it triggered? |
---|---|
|
Before injecting property values into a newly loaded entity instance |
|
Before updating an entity in the database |
| |
|
Before inserting an entity in the database |
|
After an entity instance is fully loaded |
|
After an entity is updated in the database |
|
After an entity is deleted from the database |
|
After an entity is inserted in the database |
All the preceding event listeners are available in the NHibernate.Event
namespace. Next we would implement one of the event listeners to see how to work with them.
We would implement an audit trail feature by making use of event listeners. Audit trail is information associated with every instance of entity that tells us more about how the entity instance has changed over time. Audit trail is very important in some business domains such as finances, trading, insurance, and so on. I would say, no matter what business domain you are working in, audit trail is an important piece of information that you should always try to maintain. It is useful in debugging data issues when the wrong data gets updated or even worse, when critical data gets deleted. Audit trail comes in different varieties, from most basic to most extensive. In the most basic form, it would consist of only the following information:
On the other hand, an extensive audit trail would try to capture original and modified values of every property on the entity, as and when the entity instances are modified. It is possible to implement the extensive audit trail by using event listeners but we would only look at a simple implementation to see how the event listeners work. You are encouraged to explore this topic more if you want to build a complete audit trail. The audit trail that we would implement would only record when the entity instance was created.
Let's begin by adding a DateTime
type of field on EntityBase<T>
to capture this information. We want audit trail for all our entities and hence have chosen to add this field on EntityBase<T>
so that it is accessible to all of the entities. Following code listing shows the definition of this new property:
public virtual DateTime CreatedAt { get; set; }
We now need to map this property. Since we have not mapped EntityBase<T>
explicitly, we need to map this property in every entity individually. Let's map it inside the EmployeeMappings
class, as shown next:
Property(e => e.CreatedAt);
You may find that it is repetitive to map this property in mapping of every entity. This would become even more frustrating when you would have a full list of audit trail properties that need to be mapped in every entity. To avoid this, you can map the audit trail properties as components. Basically, you define a separate AuditTrail
class that holds all the audit trail properties. Add a property of type AuditTrail
in EntityBase<T>
and then map that property as a component. You still need to map this component in every entity mapping but you are at least not repeating the mapping of a bunch of properties everywhere.
Let's now turn our attention to implementing the event listener. We want to set the CreatedAt
property before the entity is saved in the database. So a preinsert event listener is the best choice. This event listener can be implemented by extending IPreInsertEventListener
, as shown next:
public class AuditableEventListener : IPreInsertEventListener { public bool OnPreInsert(PreInsertEvent @event) { var auditInfo = @event.Entity as IAmAudited; if (auditInfo == null) return false; var createdAtPropertyIndex = Array.IndexOf(@event.Persister.PropertyNames, "CreatedAt"); if (createdAtPropertyIndex == -1) return false; var createdAt = DateTime.Now; @event.State[createdAtPropertyIndex] = createdAt; auditInfo.CreatedAt = createdAt; return false; } }
There is a lot going on in the preceding code, with a lot of things that look new and strange. Let's dissect it so that it no more remains a magic. First of all, as part of implementing IPreInsertEventListener,
we have implemented a method named OnPreInsert
.
This method takes PreInsertEvent
as an input and returns a Boolean as output. The PreInsertEvent
object holds information about the entity being inserted. The Boolean value that this function returns is used to determine whether to go ahead with insert operation or not. This is called veto internally. So if you return a true from this function, you are effectively telling NHibernate to veto the operation (reject insert operation). Since we do not want to cancel the insert operation, we return false from all the return points in this function.
In the first line of the function, the Entity
property on event object is typecast to an interface named IAmAudited
. The Entity
property holds the actual instance of the entity that was passed to the ISession.Save
method. Since our event listener is a generic implementation for any entity, we do not know the type of the entity that we are dealing with here. We need to know the type in order to access the CreatedAt
property on the entity. Hence, we have performed a little trick. We moved the CreatedAt
property declared in EntityBase<T>
into an interface named IAmAudited
. Following is how the interface looks:
public interface IAmAudited { DateTime CreatedAt { get; set; } }
We then implemented this interface on EntityBase<T>
. That is why we can now typecast any entity that we receive inside the event listener to IAmAudited
. Next line is just a safety check to make sure that we have got an instance of IAmAudited
. If not, we return a false without doing anything. The line after that uses the Array.IndexOf
method to find out index of the CreatedAt
property in the PropertyNames
array. This is where things get interesting.
There are three properties on PreInsertEvent
that are useful to us. First one is Entity
that we just looked at. Second one is Persister
, which holds information about mapping of the entity. Persister
holds the list of mapped properties on the entity inside an array named ProprtyNames
. Third is the State
array, which holds the values of each property in the PropertyNames
array. Our intention is to set correct value for the CreatedAt
property inside the State
array. That is why we first get the index of that property, and then using that index we set the value in the State
array. We then also set the CreatedAt
property on the IAmAudited
instance that we typecast. We have to do this because the State
array is populated using information from Entity
before control was passed to the event listener. If we alter the State
array at this stage then the entity instance tracked by the session and the State
array goes out of sync, leading to subtle issues that could be very difficult to track down.
We now need to let NHibernate know of our little event listener. We do that by hooking our event listener into NHibernate configuration using the appropriate event handler. Following code snippet shows how to do that:
config.EventListeners.PreInsertEventListeners = new IPreInsertEventListener[] {new AuditableEventListener()};
The Configuration
class has an EventListeners
property, which has further properties to hold event listeners for all types of supported events. We are creating an array of IPreInsertEventListener
containing only the instance of AuditableEventListener
and assigning it to the PreInsertEventListeners
property on EventListeners
.
For the sake of simplicity, we set PreInsertEvenListeners
to a brand new array of the IPreInsertEventLister
instances. It is possible that PreInsertEventListeners
already had other event listeners assigned to it, either by NHibernate itself or our own code. If that is the case then it is best to add the new even listeners to the existing array instead of overwriting it.
That is all. The event listener is wired up and it is ready to roll. We can use the following test to verify that the CreatedAt
property is actually set when an Employee
instance is saved:
[Test] public void CreatedAtIsSetWhenSavingNewInstance() { object id = 0; using (var tx = Session.BeginTransaction()) { id = Session.Save(new Employee { Firstname = "John", Lastname = "Smith" }); tx.Commit(); } Session.Clear(); using (var tx = Session.BeginTransaction()) { var employee = Session.Get<Employee>(id); Assert.That(employee.CreatedAt, Is.Not.EqualTo(DateTime.MinValue)); tx.Commit(); } }
The preceding test should not need much explanation.
Other event listeners work almost the same way as pre-insert event listener works. So you can take this understanding and apply it to most of the other event listeners with little modification. The way we used pre-insert event listener to set audit trail before any entity was inserted into the database, in the same way we can use pre-update event listener to set audit trail around the modification of entity instance. For a complete audit trail implementation, you may even store the original entity instance into some shadow database table or a completely different datastore. There are numerous options as long as you are willing to experiment.
We used the auditing of entities as an example to learn how to use event listeners. Our implementation was quite simple but a complete implementation would be slightly complex to implement. If you need such sophisticated implementation of audit trail but do not want to invest into building every moving part yourself, then take a look at a third-party library based on NHibernate, called Envers. Envers makes it very easy to audit changes to the entity instances without you having to write much code. You can find more about Envers at http://envers.bitbucket.org/.