left-icon

NHibernate Succinctly®
by Ricardo Peres

Previous
Chapter

of
A
A
A

CHAPTER 8

Interceptors and Listeners

Interceptors and Listeners


Interceptors

In this chapter, we will look at what NHibernate has to offer when it comes to changing some of its default behavior and getting notified when some events occur.

NHibernate offers a mechanism by which we can intercept, among others:

  • The creation of the SQL queries that will be sent to the database.
  • The instantiation of entity classes.
  • The indication if an entity should be persisted.
  • The detection of the dirty properties.

Interceptors are a complex mechanism. Let’s look at two simple examples—one for changing the SQL and the other for injecting behavior dynamically into entity classes loaded from records.

An interceptor is injected on the Configuration instance; only one can be applied at a time and that must be before building the session factory.

A typical implementation of a custom interceptor might inherit from the NHibernate.EmptyInterceptor class which is a do-nothing implementation of the NHibernate.IInterceptor interface:

public class SendSqlInterceptor : EmptyInterceptor

{

  private readonly Func<String> sqlBefore = null;

  private readonly Func<String> sqlAfter = null;

 

  public SendSqlInterceptor(Func<String> sqlBefore, Func<String> sqlAfter = null)

  {

    this.sqlBefore = sqlBefore;

    this.sqlAfter = sqlAfter;

  }

 

  public override SqlString OnPrepareStatement(SqlString sql)

  {

    sql = sql.Insert(0, String.Format("{0};"this.sqlBefore()));

 

    if (this.sqlAfter != null)

    {                

      sql = sql.Append(String.Format(";{0}"this.sqlAfter()));

    }

 

    return (base.OnPrepareStatement(sql));

  }

}

Tip: You need to reference the NHibernate.SqlCommand namespace for the SqlString class.

This simple example allows you to send SQL commands before and, optionally, after any other:

cfg.SetInterceptor(new SendSqlInterceptor(() => "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"

() => "DECLARE @msg NVARCHAR(100) = 'Query run at ' + CAST(GETDATE() AS VARCHAR) + ' with ' + @@ROWCOUNT + ' records'; EXEC xp_logevent 60000, @msg, 0"));

For more complex scenarios, you would have to parse the SqlString parameter and either insert, remove or replace any contents on your own.

A more interesting example is making use of NHibernate’s built-in proxy generator—the one it uses to build lazy loading proxies. This is a way to automatically add a proper implementation of INotifyPropertyChanged. You might be familiar with this interface, which is used, for example, in WPF data binding where a control needs to be notified of any changes that occur to its data source’s properties so that it can redraw itself. Implementing INotifyPropertyChanged has absolutely no complexity but it is a lot of work if we have many properties. Besides that, it forces us to use backing fields for properties. Here is the code for an interceptor that makes all loaded entities implement INotifyPropertyChanged:

public sealed class NotifyPropertyChangedInterceptor : EmptyInterceptor

{

  class _NotifyPropertyChangedInterceptor : NHibernate.Proxy.DynamicProxy.IInterceptor

  {

    private PropertyChangedEventHandler changed = delegate { };

    private readonly Object target = null;

 

    public _NotifyPropertyChangedInterceptor(Object target)

    {

      this.target = target;

    }

 

    #region IInterceptor Members

 

    public Object Intercept(InvocationInfo info)

    {

      Object result = null;

 

      if (info.TargetMethod.Name == "add_PropertyChanged")

      {

          PropertyChangedEventHandler propertyChangedEventHandler = info.Arguments[0] 

as PropertyChangedEventHandler;

        this.changed += propertyChangedEventHandler;

      }

      else if (info.TargetMethod.Name == "remove_PropertyChanged")

      {

        PropertyChangedEventHandler propertyChangedEventHandler = info.Arguments[0] 

as PropertyChangedEventHandler;

        this.changed -= propertyChangedEventHandler;

      }         

      else

      {

        result = info.TargetMethod.Invoke(this.target, info.Arguments);

      }

 

      if (info.TargetMethod.Name.StartsWith("set_") == true)

      {

        String propertyName = info.TargetMethod.Name.Substring("set_".Length);

        this.changed(info.Target, new PropertyChangedEventArgs(propertyName));

      }

 

      return (result);

    }

    #endregion

  }

 

  private ISession session = null;

  private static readonly ProxyFactory factory = new ProxyFactory();

 

  public override void SetSession(ISession session)

  {

    this.session = session;

    base.SetSession(session);

  }

 

  public override Object Instantiate(String clazz, EntityMode entityMode, Object id)

  {

    Type entityType = this.session.SessionFactory.GetClassMetadata(clazz).GetMappedClass(

entityMode);

    Object target = this.session.SessionFactory.GetClassMetadata(entityType).Instantiate(id, 

entityMode);

    Object proxy = factory.CreateProxy(entityType, new _NotifyPropertyChangedInterceptor(target), 

typeof(INotifyPropertyChanged));

    this.session.SessionFactory.GetClassMetadata(entityType).SetIdentifier(proxy, id, entityMode);

    return (proxy);

  }

}

Tip: You need to reference the NHibernate, NHibernate.Type, System.ComponentModel, and System.Reflection namespaces.

Its registration is as simple as:

cfg.SetInterceptor(new NotifyPropertyChangedInterceptor());

And a sample usage:

Product p = session.Query<Product>().First();

INotifyPropertyChanged npc = p as INotifyPropertyChanged;

npc.PropertyChanged += delegate(Object sender, PropertyChangedEventArgs args)

{

  //…

};

 

p.Price *= 10; //raises the NotifyPropertyChanged event

Granted, this code is a bit complex. Nevertheless, it isn’t hard to understand:

  • The Instantiate method is called when NHibernate is going to create an object instance for a record obtained from the database. In the base EmptyInterceptor class it just returns null by default so NHibernate knows it must create it by itself.
  • In our own implementation, we ask NHibernate to create an instance the way it would normally do, by calling IClassMetadata.Instantiate.
  • NHibernate’s ProxyFactory will then create a new proxy for the desired class, have it implement INotifyPropertyChanged, and pass it an implementation of a proxy interceptor, _NotifyPropertyChangedInterceptor, which will handle all requests for virtual or abstract methods and properties and, in the event of a setter call (identified by the prefix “set_” or an event registration – “add_”) will execute some custom code.
  • Because the generated proxy will be of a class that inherits from an entity’s class, it must not be marked as sealed and all of its properties and methods must be virtual.

Listeners

Listeners are NHibernate’s events; they allow us to be notified when something occurs. It turns out that NHibernate offers a very rich set of events that cover just about anything you might expect—from entity loading, deletion and saving, to session flushing and more.

Multiple listeners can be registered for the same event; they will be called synchronously at specific moments which are described in the following table. The table lists both the code name as well as the XML name of each event. I have also included the name of the property in the Configuration.EventListeners property where the listeners can be added by code.

The full list of events is:

  1. NHibernate Events

Event

Description and Registration Property

Autoflush/auto-flush

Called when the session is flushed automatically (AutoFlushEventListeners property)

Create/create

Called when an instance is saved (PersistEventListeners)

CreateOnFlush/

create-onflush

Called when an instance is saved automatically by a Flush operation (PersistOnFlushEventListeners)

Delete/delete

Called when an entity is deleted by a call to Delete (DeleteEventListeners)

DirtyCheck/dirty-check

Called when a session is being checked for dirty entities (DirtyCheckEventListeners)

Evict/evict

Called when an entity is being evicted from a session (EvictEventListeners)

Flush/flush

Called when a Flush call occurs or a transaction commits, after FlushEntity is called for each entity in the session (FlushEventListeners)

FlushEntity/flush-entity

Called for each entity present in a session when it is flushed (FlushEntityEventListeners)

Load/load

Called when a session is loaded either by the Get/Load method or by a query, after events PreLoad and PostLoad (LoadEventListeners)

LoadCollection/

load-collection

Called when an entity’s collection is being populated (InitializeCollectionEventListeners)

Lock/lock

Called when an entity and its associated record are being locked explicitly, either by an explicit call to the Lock method or by passing a LockMode in a query (LockEventListeners)

Merge/merge

Called when an existing entity is being merged with a disconnected one, usually by a call to Merge (MergeEventListeners)

{Pre/Post}CollectionRecreate/{pre/post}-collection-recreate

Called before/after a bag is being repopulated, after its elements have changed ({Pre/Post}CollectionRecreateEventListeners)

{Pre/Post}CollectionRemove/{pre/post}-collection-remove

Called before/after an entity is removed from a collection ({Pre/Post}CollectionRemoveEventListeners)

{Pre/Post}CollectionUpdate/{pre/post}-collection-update

Called before/after a collection was changed ({Pre/Post}CollectionUpdateEventListeners)

PostCommitDelete/

post-commit-delete

Called after a delete operation was committed (PostCommitDeleteEventListeners)

PostCommitInsert/

post-commit-insert

Called after an insert operation was committed (PostCommitInsertEventListeners)

PostCommitUpdate/

post-commit-update

Called after an update operation was committed (PostCommitUpdateEventListeners)

{Pre/Post}Delete/

{pre/post}-delete

Called before/after a delete operation ({Pre/Post}DeleteEventListeners)

{Pre/Post}Insert/{pre/post}-insert

Called before/after an insert operation ({Pre/Post}InsertEventListeners)

{Pre/Post}Load/{pre/post}-load

Called before/after a record is loaded and an entity instance is created ({Pre/Post}LoadEventListeners)

{Pre/Post}Update/{pre/post}-update

Called before/after an instance is updated ({Pre/Post}UpdateEventListeners)

Refresh/refresh

Called when an instance is refreshed (RefreshEventListeners)

Replicate/replicate

Called when an instance is being replicated (ReplicateEventListeners)

Save/save

Called when an instance is being saved, normally by a call to Save or SaveOrUpdate but after PostInsert/PostUpdate (SaveEventListeners)

SaveUpdate/save-update

Called when an instance is being saved, normally by a call to SaveOrUpdate but after PostUpdate (SaveOrUpdateEventListeners)

Update/update

Called when an instance is being updated explicitly, by a call to Update (UpdateEventListeners)

An event listener needs to be registered in the Configuration instance prior to creating a session factory from it:

//register a listener for the FlushEntity event

cfg.AppendListeners(ListenerType.FlushEntity, new IFlushEntityEventListener[]{     

new ProductCreatedListener() });

It is also possible to register event handlers by XML configuration; make sure you add an assembly qualified type name:

<session-factory>

  <!-- … -->

  <listener type="flush-entity" class="Succinctly.Console.ProductCreatedListener, Succinctly.Console"/>

</session-factory>

Let’s look at two examples, one for firing a domain event whenever a new product is added and the other for adding auditing information to an entity.

Here’s the first listener:

public class ProductCreatedListener : IFlushEntityEventListener

{

  public static event Action<Product> ProductCreated;

 

  #region IFlushEntityEventListener Members

     

  public void OnFlushEntity(FlushEntityEvent @event)

  {

    if (@event.Entity is Product)

    {

      if (ProductCreated != null)

      {

        ProductCreated(@event.Entity as Product);

      }

    }

  }

 

  #endregion

}

Tip: Add a using declaration for namespace NHibernate.Event.

An an example usage:

//register a handler for the ProductCreated event

ProductCreatedListener.ProductCreated += delegate(Product p)

{

  Console.WriteLine("A new product was saved");

};

 

//register a listener for the FlushEntity event

cfg.AppendListeners(ListenerType.FlushEntity, new IFlushEntityEventListener[]{ 

new ProductCreatedListener() });

 

//a sample product

Product product = new Product() { Name = "Some Product", Price = 100, Specification = 

XDocument.Parse("<data/>") };

//save the new product

session.Save(product);

session.Flush();      //the ProductCreatedListener.ProductCreated event will be raised here

As for the auditing, let’s start by defining a common interface:

public interface IAuditable

{   

  String CreatedBy { get; set; }

  DateTime CreatedAt { get; set; }

  String UpdatedBy { get; set; }

  DateTime UpdatedAt { get; set; }

}

The IAuditable interface defines properties for storing the name of the user who created and last updated a record, as well as the date and time of its creation and last modification. The concept should be familiar to you. Feel free to add this interface to any of your entity classes.

Next, the listener that will handle NHibernate events and fill in the auditing information:

public class AuditableListener : IFlushEntityEventListenerISaveOrUpdateEventListener

IMergeEventListener

{

  public AuditableListener()

  {

    this.CurrentDateTimeProvider = () => DateTime.UtcNow;

    this.CurrentIdentityProvider = () => WindowsIdentity.GetCurrent().Name;

  }

 

  public Func<DateTime> CurrentDateTimeProvider { get; set; }

  public Func<String> CurrentIdentityProvider { get; set; }

 

  protected void ExplicitUpdateCall(IAuditable trackable)

  {

    if (trackable == null)

    {

      return;

    }

 

    trackable.UpdatedAt = this.CurrentDateTimeProvider();

    trackable.UpdatedBy = this.CurrentIdentityProvider();

 

    if (trackable.CreatedAt == DateTime.MinValue)

    {

      trackable.CreatedAt = trackable.UpdatedAt;

      trackable.CreatedBy = trackable.UpdatedBy;

    }

  }

 

  protected Boolean HasDirtyProperties(FlushEntityEvent @event)

  {

    if ((@event.EntityEntry.RequiresDirtyCheck(@event.Entity) == false

     || (@event.EntityEntry.ExistsInDatabase == false) || (@event.EntityEntry.LoadedState == null))

    {

      return (false);

    }

 

    Object[] currentState = @event.EntityEntry.Persister

.GetPropertyValues(@event.Entity, @event.Session.EntityMode);

    Object[] loadedState = @event.EntityEntry.LoadedState;

 

    return (@event.EntityEntry.Persister.EntityMetamodel.Properties.Where((property, i) => 

      (LazyPropertyInitializer.UnfetchedProperty.Equals(currentState[i]) == false

      && (property.Type.IsDirty(loadedState[i], currentState[i], @event.Session) == true))

      .Any());

  }

 

  public void OnFlushEntity(FlushEntityEvent @event)

  {

    if ((@event.EntityEntry.Status == Status.Deleted) || (@event.EntityEntry.Status == Status.ReadOnly))

    {

      return;

    }

 

    IAuditable trackable = @event.Entity as IAuditable;

     

    if (trackable == null)

    {

      return;

    }

 

    if (this.HasDirtyProperties(@event) == true)

    {

      this.ExplicitUpdateCall(trackable);

    }

  }

 

  public void OnSaveOrUpdate(SaveOrUpdateEvent @event)

  {

    IAuditable auditable = @event.Entity as IAuditable;

    if ((auditable != null) && (auditable.CreatedAt == DateTime.MinValue))

    {

      this.ExplicitUpdateCall(auditable);

    }

  }

 

  public void OnMerge(MergeEvent @event)

  {

    this.ExplicitUpdateCall(@event.Entity as IAuditable);

  }

 

  public void OnMerge(MergeEvent @event, IDictionary copiedAlready)

  {

    this.ExplicitUpdateCall(@event.Entity as IAuditable);

  }

}

As for the registration code, it is a little more complex than the previous example:

AuditableListener listener = new  AuditableListener();

cfg.AppendListeners(ListenerType.Save, new ISaveOrUpdateEventListener[] { listener });      cfg.AppendListeners(ListenerType.SaveUpdate, new ISaveOrUpdateEventListener[] { listener });

cfg.AppendListeners(ListenerType.Update, new ISaveOrUpdateEventListener[] { listener });

cfg.AppendListeners(ListenerType.FlushEntity, new IFlushEntityEventListener[] { listener });

cfg.AppendListeners(ListenerType.Merge, new IMergeEventListener[] { listener });

In XML:

<listener type="save" class="Succinctly.Common.AuditableListener, Succinctly.Common"/>

<listener type="save-update" class="Succinctly.Common.AuditableListener, Succinctly.Common"/>

<listener type="update" class="Succinctly.Common.AuditableListener, Succinctly.Common"/>

<listener type="flush-entity" class="Succinctly.Common.AuditableListener, Succinctly.Common"/>

<listener type="merge" class="Succinctly.Common.AuditableListener, Succinctly.Common"/>

The AuditableListener class allows you to specify a delegate property for obtaining the current date and time (CurrentDateTimeProvider) and the name of the current user (CurrentIdentityProvider). It must be registered as a listener for several events (Save, SaveOrUpdate, Update, FlushEntity, and Merge) because several things can happen:

  • An entity can be marked for saving (Save).
  • An entity can be marked for saving or updating (SaveOrUpdate).
  • An entity can be updated explicitly (Update).
  • A disconnected entity may be merged with an existing one, thus possibly changing it (Merge).
  • An entity that is dirty may reach a session flush (FlushEntity).
Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.