In an application you would like to have maximum control over interaction with the database. In the ideal situation there is one single point where all data can be checked and monitored before it is sent to, or read from the database. In nHibernate EventListeners are an ideal way to do that. Every entity read from or sent to the db, whether explicitly flushed or part of a graph, passes there. nHibernate has a long list of Events you can listen to. At first sight the documentation on picking the right listeners and how to implement them points to an article by Ayende. Alas there are some severe issues taking that direction. There is a better way.
The problem
Entities in our domain can have quite complex validation. My base DomainObject class has a string-list of possible Issues, and a Validate method. An object with an empty issue list is considered valid. What makes an object invalid is described in the issue-list. Implementing this validation in the nHibernate OnPreUpdate event listener would seem a solid way to trap all validation errors.
- public bool OnPreUpdate(PreUpdateEvent preUpdateEvent)
- {
- var domainObject = preUpdateEvent.Entity as DomainObject;
- if (domainObject == null)
- return false;
- domainObject.Validate();
- if (domainObject.Issues.Any())
- throw new InvalidDomainObjectException(domainObject);
- return false;
- }
Pretty straigthforward. The Validate method performs the validation. In case this results in any issues an exception is thrown and the update is canceled. But there is a huge problem with this approach. As the validation code can, and will, do almost anything there is a chance it will touch a lazy collection. Resulting in an nHibernate exception. It is performing a flush, the lazy collection will trigger a read. This results in the dreaded “Collection was not processed in flush” exception.
The way out
There are loads and loads of events your code can listen to. The OnFlushEntity event is fired before the OnPreUpdate event. It fires at the right occasion and the best part is that you can touch anything while inside.
- public void OnFlushEntity(FlushEntityEvent flushEntityEvent)
- {
- if (HasDirtyProperties(flushEntityEvent))
- {
- var domainObject = flushEntityEvent.Entity as DomainObject;
- if (domainObject != null)
- domainObject.Validate();
- }
- }
- }
The validation is only performed, I leave the rejection of invalid entities in the OnPreUpdate event.
- public bool OnPreUpdate(PreUpdateEvent preUpdateEvent)
- {
- var domainObject = preUpdateEvent.Entity as DomainObject;
- if (domainObject == null)
- return false;
- if (domainObject.Issues.Any())
- throw new InvalidDomainObjectException(domainObject);
- return false;
- }
Crucial in the OnFlushEntity event is the HasDirtyProperties method. This method was found here, in a GitHub contribution by Filip Kinsky, just about the only documentation on the event.
- private bool HasDirtyProperties(FlushEntityEvent flushEntityEvent)
- {
- ISessionImplementor session = flushEntityEvent.Session;
- EntityEntry entry = flushEntityEvent.EntityEntry;
- var entity = flushEntityEvent.Entity;
- if (!entry.RequiresDirtyCheck(entity) || !entry.ExistsInDatabase || entry.LoadedState == null)
- {
- return false;
- }
- IEntityPersister persister = entry.Persister;
- object[] currentState = persister.GetPropertyValues(entity, session.EntityMode);
- object[] loadedState = entry.LoadedState;
- return persister.EntityMetamodel.Properties
- .Where((property, i) => !LazyPropertyInitializer.UnfetchedProperty.Equals(currentState[i]) && property.Type.IsDirty(loadedState[i], currentState[i], session))
- .Any();
- }
As stated, you can do almost anything in the OnFlushEntity event, modifying data in entities included. So this is an ideal place to set auditing properties, or even add items to collections of the entity. All these modifications will be persisted to the database.
The original post by Ayende was on setting auditing properties, not about validation. Modifying an entity inside the OnPreUpdate event can be done, but takes some fiddling. Having discovered OnFlushEntity we moved not only the validation but also the auditing code here.
Setting eventhandlers
So far I have described the event handlers but have not shown yet how to hook them up. The eventhandlers are defined in interfaces, to be implemented in a class. The snippets above are all members of one class.
- public class RechtenValidatieEnLogListener : IPreUpdateEventListener, IPreInsertEventListener, IPreDeleteEventListener, IPostLoadEventListener, IFlushEntityEventListener
- {
- // ……
- }
Used when creating the sessionfactory.
- private static ISessionFactory GetFactory()
- {
- var listener = new RechtenValidatieEnLogListener();
- return Fluently.Configure().
- Database(CurrentConfiguration).
- Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())).
- ExposeConfiguration(c => listener.Register(c)).
- CurrentSessionContext<HybridWebSessionContext>().
- BuildSessionFactory();
- }
Registering the handlers is done by the Register method, which uses the configuration
- public void Register(Configuration cfg)
- {
- cfg.EventListeners.FlushEntityEventListeners = new[] { this }
- .Concat(cfg.EventListeners.FlushEntityEventListeners)
- .ToArray();
- cfg.EventListeners.PreUpdateEventListeners = new[] { this }
- .Concat(cfg.EventListeners.PreUpdateEventListeners)
- .ToArray();
- }
Most examples on hooking in handlers use the cfg.SetListener method. The problem with that is that it knocks out any handlers already hooked in. For the OnPreUpdate event that’s no problem, but knocking out the default OnFlushEntity event is fatal. Using this code your custom listener will be combined with any handlers already set.
Winding down
That’s all there is to it. I have left out the parts on validating reads, inserts or deletes. They follow the same pattern, up to you to implement them. EventListeners are very powerful, but it is a pity the documentation is so sparse. All of this was found by scraping the web and a lot of trial and error. But now we have a very solid system for validation and auditing. Without any limitations.
