Event system

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 listeners

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?

IPreLoadEventListener

Before injecting property values into a newly loaded entity instance

IPreUpdateEventListener

Before updating an entity in the database

IPreDeleteEventListener

Before deleting an entity from the database

IPreInsertEventListener

Before inserting an entity in the database

IPostLoadEventListener

After an entity instance is fully loaded

IPostUpdateEventListener

After an entity is updated in the database

IPostDeleteEventListener

After an entity is deleted from the database

IPostInsertEventListener

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.

Adding the audit trail for entities

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:

  • When was the instance created?
  • Who created the instance?
  • When was the instance last updated?
  • Who updated the instance last?

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);

Note

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.

Note

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.

Note

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset