Click here to Skip to main content
Click here to Skip to main content

Sample NHibernate IInterceptor implementation

, 6 Feb 2007 CPOL
Rate this:
Please Sign up or sign in to vote.
This article describes a simple implementation of NHibernate IInterceptor interface

Introduction

Audit logging is an important issue while building enterprise systems. The simplest form of audit logging is recording who created/updated an object or a record in the database respectively and when it was done. We perform four basic operations on a domain object. These are:

  • Load
  • Update
  • Save
  • Delete

Data access layer is a good place to perform automated audit logging whenever one of the operations above is executed. NHibernate provides us with the IInterceptor interface plus ILifecylce interface. You can read this article for more information about NHibernate entity lifecycle management.

In this article we will try to perform simple logging that meets the following minimal requirements:

  1. We will log who performed insert/update and when this operation was performed
  2. Log data will be written to the same database and table as our domain object

Class Model

We have three interfaces directly related to audit logging and IInterceptor implementation, one interface (IVersionedEntity ) which is in the model just for conceptual completeness and is used to indicate that we want to utilize NHibernate managed versioning, one base DomainObject from which all our domain objects inherit and finally three implementation classes that implement IInsertLoggable, IModifyLoggable and IUpdateLoggable interfaces.

Sample image

public abstract class InsertLoggableDomainObject<idt /> : 
            DomainObject<idt />,IVersionedEntity, IInsertLoggable
{
  #region IVersionedEntity Members

  private long _version = 0;
  public long Version
  {
    get { return _version; }
  }

  #endregion

  #region IInsertLogable Members

  private DateTime? _sysCreatedOn = null;
  public DateTime? SysCreatedOn
  {
    get { return _sysCreatedOn; }
    set { _sysCreatedOn = value; }
  }

  private int? _sysCreatedBy = null;
  public int? SysCreatedBy
  {
    get { return _sysCreatedBy; }
    set { _sysCreatedBy = value; }
  }
  #endregion
}

public abstract class UpdateLoggableDomainObject<idt /> : 
            DomainObject<idt />, IVersionedEntity, IUpdateLoggable
{
  #region IVersionedEntity Members

  private long _version = 0;
  public long Version
  {
    get { return _version; }
  }

  #endregion

  #region IUpdateLogable Members

  private DateTime? _sysLastUpdatedOn = null;
  public DateTime? SysLastUpdatedOn
  {
    get { return _sysLastUpdatedOn; }
    set { _sysLastUpdatedOn = value; }
  }

  private int? _sysLastUpdatedBy = null;
  public int? SysLastUpdatedBy
  {
    get { return _sysLastUpdatedBy; }
    set { _sysLastUpdatedBy = value; }
  }
  #endregion
}

public abstract class ModifyLoggableDomainObject<idt /> : 
            DomainObject<idt />, IVersionedEntity, IModifyLogable
{
  #region IVersionedEntity Members

  private long _version = 0;
  public long Version
  {
    get { return _version; }
  }

  #endregion

  #region IModifyLogable Members

  private DateTime? _sysCreatedOn = null;
  public DateTime? SysCreatedOn
  {
    get { return _sysCreatedOn; }
    set { _sysCreatedOn = value; }
  }

  private int? _sysCreatedBy = null;
  public int? SysCreatedBy
  {
    get { return _sysCreatedBy; }
    set { _sysCreatedBy = value; }
  }

  private DateTime? _sysLastUpdatedOn = null;
  public DateTime? SysLastUpdatedOn
  {
    get { return _sysLastUpdatedOn; }
    set { _sysLastUpdatedOn = value; }
  }

  private int? _sysLastUpdatedBy = null;
  public int? SysLastUpdatedBy
  {
    get { return _sysLastUpdatedBy; }
    set { _sysLastUpdatedBy = value; }
  }
  #endregion
}

If we want our domain objects to be audit logged by our NHibernate IInterceptor instead of solely inheriting from DomainObject, we may inherit our domain objects from one of the loggable domain object implementations (InsertLoggableDomainObject, UpdateLoggableDomainObject, ModifyLoggableDomainObject).

NHibernate Setup

Inheriting our domain objects from one of the base loggable classes does not provide us
full logging support.

NHibernate Mappings

As any other NHibernate utilization needs some mapping work this sample also needs some NHibernate mapping.
All our loggable domain objects must include the following mapping information:

<?xml version='1.0' encoding='utf-8'?>
<hibernate-mapping
    xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
    xmlns:xsd='http://www.w3.org/2001/XMLSchema' 
                xmlns='urn:nhibernate-mapping-2.0'>
  <class
      name='Fully Qualified Class name comes here'
      table='Table name'>
    <id name='Identity field name'
        column='Identity column to which identity field maps'
        unsaved-value='0'>
      <generator class='identity'/>
    </id>
    <!--Managed versioning support. Defined in IVersionedEntity interface -->
    <version name='_version' column='Version' access='field' 
                    unsaved-value='0' type='Int64'/>

    <!--Log field mappings -->
    <property name='SysCreatedOn' column='SysCreatedOn' 
            type='NHibernate.Nullables2.NullableDateTimeType, 
            NHibernate.Nullables2'/>
    <property name='SysCreatedBy' column='SysCreatedBy' 
            type='NHibernate.Nullables2.NullableInt32Type, 
            NHibernate.Nullables2'/>
    <property name='SysLastUpdatedOn' column='SysLastUpdatedOn' 
            type='NHibernate.Nullables2.NullableDateTimeType, 
            NHibernate.Nullables2'/>
    <property name='SysLastUpdatedBy' column='SysLastUpdatedBy' 
            type='NHibernate.Nullables2.NullableInt32Type, 
            NHibernate.Nullables2'/>
    <!-- Some other field, relation, subclass and other kind of mappings 
                        from this point on-->
  </class>
</hibernate-mapping>

We use NHibernate.Nullables2 (found in NHibernate Contributions) for type definitions of our log data fields.
We can omit this reference and usage of nullables by simply changing interface field definitions and their implementations from int? to int and DateTime? to DateTime. But be warned that SysCreatedBy and SysLastUpdatedBy columns are defined as foreign keys to another table in the database, say Person. Thus making these fields non-nullable may cause constraint violations on the database side.

NHibernate Session Initialization

This step is simple. Either you directly manage NHibernate sessions from your own code
or you use simple but yet very powerful NHSessionManager singleton. You have to use ISession ISessionFactory.OpenSession(IInterceptor interceptor)
overload from NHibernate assembly to open a session. IInterceptor interceptor< parameter here is an instance of our IInterceptor implementation.

IInterceptor Implementation

You can think of an interceptor as a hook utility to the data access layer. IInterceptor implementation will look like this:

public class MyAuditLogger:IInterceptor
{
  #region IInterceptor Members

  public int[] FindDirty(object entity, object id, 
            object[] currentState, object[] previousState, 
            string[] propertyNames, 
            global::NHibernate.Type.IType[] types)
  {
      return null;
  }

  public object Instantiate(Type type, object id)
  {
      return null;                
  }

  public object IsUnsaved(object entity)
  {
      return null;
  }

  public void OnDelete(object entity, object id, object[] state, 
    string[] propertyNames, global::NHibernate.Type.IType[] types)
  {
  }

  public bool OnFlushDirty(object entity, object id, object[] currentState, 
        object[] previousState, string[] propertyNames, 
        global::NHibernate.Type.IType[] types)
  {
      if (entity is IUpdateLogable)
      {
          SetUpdateLoggableValues(currentState, propertyNames);
          return true;
      }
      else if (entity is IModifyLogable)
      {
          SetModifyLoggableValues_OnUpdate(currentState, propertyNames);
          return true;
      }
      else
      {
          return true;
      }
  }

  public bool OnLoad(object entity, object id, object[] state, 
    string[] propertyNames, global::NHibernate.Type.IType[] types)
  {
      return true;
  }

  public bool OnSave(object entity, object id, object[] state, 
    string[] propertyNames, global::NHibernate.Type.IType[] types)
  {
      if (entity is IInsertLogable)
      {
          SetInsertLoggableValues(state, propertyNames);
          return true;
      }
      else if (entity is IModifyLogable)
      {
          SetModifyLoggableValues_OnInsert(state, propertyNames);
          return true;
      }
      else
      {
          return true;
      }
  }

  public void PostFlush(System.Collections.ICollection entities)
  {

  }

  public void PreFlush(System.Collections.ICollection entities)
  {

  }

  private Hashtable GetInsertLoggablePropertyIndexes(string[] Properties)
  {
      Hashtable result = new Hashtable();
      int propCounter = 0;
      for (int i = 0; i < Properties.Length; i++)
      {
          if (Properties[i].ToLower() == "syscreatedby")
          {
              propCounter++;
              result.Add("syscreatedby", i);
          }
          else if (Properties[i].ToLower() == "syscreatedon")
          {
              propCounter++;
              result.Add("syscreatedon", i);
          }

          if (propCounter == 2)
          {
              break;
          }
      }
      return result;  
  }

  private Hashtable GetUpdateLoggablePropertyIndexes(string[] Properties)
  {
      Hashtable result = new Hashtable();
      int propCounter = 0;
      for (int i = 0; i < Properties.Length ; i++)
      {
          if (Properties[i].ToLower() == "syslastupdatedby")
          {
              propCounter++;
              result.Add("syslastupdatedby", i);
          }
          else if (Properties[i].ToLower() == "syslastupdatedon")
          {
              propCounter++;
              result.Add("syslastupdatedon", i);
          }

          if (propCounter == 2)
          {
              break;
          }
      }
      return result;
  }

  private Hashtable GetModifyLoggablePropertyIndexes(string[] Properties)
  {
      Hashtable result = new Hashtable();
      int propCounter = 0;
      for (int i = 0; i < Properties.Length; i++)
      {
          if (Properties[i].ToLower() == "syscreatedby")
          {
              propCounter++;
              result.Add("syscreatedby", i);
          }
          else if (Properties[i].ToLower() == "syscreatedon")
          {
              propCounter++;
              result.Add("syscreatedon", i);
          }
          else if (Properties[i].ToLower() == "syslastupdatedby")
          {
              propCounter++;
              result.Add("syslastupdatedby", i);
          }
          else if (Properties[i].ToLower() == "syslastupdatedon")
          {
              propCounter++;
              result.Add("syslastupdatedon", i);
          }

          if (propCounter == 4)
          {
              break;
          }
      }
      return result;
  }

  private void SetInsertLoggableValues(object[] state, string[] Properties)
  {
      Hashtable indexes = GetInsertLoggablePropertyIndexes(Properties);
      if (indexes.Count != 2)
      {
          throw new Exception("Can't log IInsertLoggable entity. 
                        Indexes not found!");
      }

      int index = -1;
      
      if(indexes["syscreatedby"] == null)
      {
          throw new Exception("Can't log IInsertLoggable entity. 
            Index value for SysCreatedBy does not exist!");            
      }
      index = (int)indexes["syscreatedby"];
      state[index] = ContextManager.Instance.ActivePersonID;

      if (indexes["syscreatedon"] == null)
      {
          throw new Exception("Can't log IInsertLoggable entity. 
            Index value for SysCreatedOn does not exist!");
      }
      index = (int)indexes["syscreatedon"];
      state[index] = DateTime.Now;

  }

  private void SetUpdateLoggableValues(object[] state, string[] Properties)
  {
      Hashtable indexes = GetUpdateLoggablePropertyIndexes(Properties);
      if (indexes.Count != 2)
      {
          throw new Exception("Can't log IUpdateLoggable entity. 
                            Indexes not found!");
      }

      int index = -1;

      if (indexes["syslastupdatedby"] == null)
      {
          throw new Exception("Can't log IUpdateLoggable entity. 
            Index value for SysLastUpdatedBy does not exist!");
      }
      index = (int)indexes["syslastupdatedby"];
      state[index] = ContextManager.Instance.ActivePersonID;

      if (indexes["syslastupdatedon"] == null)
      {
          throw new Exception("Can't log IUpdateLoggable entity. 
            Index value for SysLastUpdatedOn does not exist!");
      }
      index = (int)indexes["syslastupdatedon"];
      state[index] = DateTime.Now;

  }

  private void SetModifyLoggableValues_OnInsert(object[] state, 
                            string[] Properties)
  {
      Hashtable indexes = GetModifyLoggablePropertyIndexes(Properties);
      if (indexes.Count != 4)
      {
          throw new Exception("Can't log IInsertLoggable entity. 
                            Indexes not found!");
      }

      int index = -1;

      if (indexes["syscreatedby"] == null)
      {
          throw new Exception("Can't log IInsertLoggable entity. 
            Index value for SysCreatedBy does not exist!");
      }
      index = (int)indexes["syscreatedby"];
      state[index] = ContextManager.Instance.ActivePersonID;

      if (indexes["syscreatedon"] == null)
      {
          throw new Exception("Can't log IInsertLoggable entity. 
            Index value for SysCreatedOn does not exist!");
      }
      index = (int)indexes["syscreatedon"];
      state[index] = DateTime.Now;

  }

  private void SetModifyLoggableValues_OnUpdate(object[] state, 
                            string[] Properties)
  {
      Hashtable indexes = GetModifyLoggablePropertyIndexes(Properties);
      if (indexes.Count != 4)
      {
          throw new Exception("Can't log IModifyLogable entity. 
                            Indexes not found!");
      }

      int index = -1;

      if (indexes["syslastupdatedby"] == null)
      {
          throw new Exception("Can't log IModifyLogable entity. 
            Index value for SysLastUpdatedBy does not exist!");
      }
      index = (int)indexes["syslastupdatedby"];
      state[index] = ContextManager.Instance.ActivePersonID;

      if (indexes["syslastupdatedon"] == null)
      {
          throw new Exception("Can't log IModifyLogable entity. 
            Index value for SysLastUpdatedOn does not exist!");
      }
      index = (int)indexes["syslastupdatedon"];
      state[index] = DateTime.Now;
  }
  
  #endregion
}

Actually we only implemented OnFlushDirty and OnSave methods of the IInterceptor and left other the interface methods to return null or empty. The following methods are utility methods that help us to find out the domain object type and fill the appropriate log data:

Property validation methods

These methods simply loop through domain object properties, already supported by NHibernate, to determine if log fields (SysCreatedBy, SysLastUpdatedBy, SysCreatedOn, SysLastUpdatedOn) have NHibernate mapping definitions. If NHibernate mapping definitions do not exist for these fields, an exception is thrown by the interceptor.

  • GetInsertLoggablePropertyIndexes
  • GetUpdateLoggablePropertyIndexes
  • GetModifyLoggablePropertyIndexes

Log data setters

These methods simply set log data (SysCreatedBy, SysLastUpdatedBy, SysCreatedOn, SysLastUpdatedOn) based on the loggable interface implemented by the domain object.

  • SetInsertLoggableValues
  • SetUpdateLoggableValues
  • SetModifyLoggableValues_OnInsert
  • SetModifyLoggableValues_OnUpdate

Remarks and Future Work Suggestions

The sample implementation was one of my first experiences with NHibernate. So it may not sound very efficient to you. But I believe this sample will be a good starting point.

I've read about some difficulties and tips about performing interceptor operations and logging using the NHibernate session that the interceptor is already attached to.

Further work can focus on logging to different databases by defining loggable domain object
implementations as reusable domain objects with their own mappings.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Ali Ozgur
Team Leader PragmaTouch
Turkey Turkey
- Software developer
- Has BS degree in Computer Engineering
- Has MBA degree
- Programmed with C, C++, Delphi, T-SQL and recently C#
- Little educational experience with Prolog
- Feel enthusiasm about NHibernate and LINQ
- Love to develop on Cuyahoga Web Framework
- Developer of PragmaSQL Editor
(Code Project Members Choice Winner for 2009 and 2010)
- Developed JiraTouch and MoodleTouch for iPhone
- PragmaTouch Lead (www.pragmatouch.com)
Follow on   Twitter

Comments and Discussions

 
QuestionDoes this work? Pinmemberneerajkamat28-Dec-09 20:23 
QuestionThanks for the article - What is ContextManager ? Pinmembergraphicsxp14-May-09 0:31 
AnswerRe: Thanks for the article - What is ContextManager ? PinmemberAli Ozgur14-May-09 2:45 
GeneralVery helpful...thanks! PinmemberBilly McCafferty12-Jan-09 19:10 
GeneralRe: Very helpful...thanks! PinmemberAli Ozgur12-Jan-09 21:23 
GeneralNew to NHibernate IInterceptor implementation Pinmembervijay d5-Jan-09 5:13 
GeneralRe: New to NHibernate IInterceptor implementation PinmemberAli Ozgur12-Jan-09 21:06 
Hi,
 
I'll try to help you if you provide me more details. Some of the key questions are
 
* Which version of NHibernate you plan to use
* What will you log into the database. For example some properties marked with an attribute or serialized object or anything else?
* What your entities look like?
* Is log database same as the operational database.
 

GeneralEmptyInterceptor class PinmemberJason Wilden24-May-08 0:24 
GeneralReuseable mapping component PinmemberMBursill26-Jul-07 11:20 
GeneralRe: Reuseable mapping component PinmemberAli Ozgur26-Jul-07 21:53 
QuestionWhat is up with the ? at the end of type fields? Pinmemberphilmee9514-May-07 12:16 
AnswerRe: What is up with the ? at the end of type fields? PinmemberAli Ozgur14-May-07 21:14 
GeneralRe: What is up with the ? at the end of type fields? Pinmemberphilmee9515-May-07 7:41 
GeneralRe: What is up with the ? at the end of type fields? Pinmembervanslly26-Jul-11 10:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.1411023.1 | Last Updated 6 Feb 2007
Article Copyright 2007 by Ali Ozgur
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid