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

Sample NHibernate IInterceptor implementation

By , 6 Feb 2007
 

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)

About the Author

Ali Ozgur
Team Leader PragmaTouch
Turkey Turkey
Member
- 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)

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionDoes this work?memberneerajkamat28 Dec '09 - 19:23 
Thanks for a great article! I was wondering if this code would work instead of iterating through the currentState? It seems a waste to iterate through the currentState and propertyNames when we already have the interface implemented. I haven't seen anyone comment on this. May be I need to look at the NHibernate code to see the problem.
 

public bool OnSave(object entity, object id, object[] state,
string[] propertyNames, global::NHibernate.Type.IType[] types)
{
IInsertLogable logable = entity as IInsertLogable;
 
if (logable != null)
{
logable.SysCreatedBy = GetUser();
logable.SysCreatedOn = GetSysDateTime();
 
return true;
}

return false;
}
QuestionThanks for the article - What is ContextManager ?membergraphicsxp13 May '09 - 23:31 
Hi,
I've found this article very useful and I'm going to use it into my project. But I don't understand this line :
state[index] = ContextManager.Instance.ActivePersonID;
 
There's no explanations about it. Do I need this or can I remove it ?
 
Thanks again.
AnswerRe: Thanks for the article - What is ContextManager ?memberAli Ozgur14 May '09 - 1:45 
You do not need state[index] = ContextManager.Instance.ActivePersonID; assignment. But please remember that state array is provided by NHibernate runtime, and if you want to change value of a mapped property of your entity with interception you have to find which element in that state array matches to your entity's property.
 
ContextManager is a special class I designed for my own use so you do not have to use that.
 

GeneralVery helpful...thanks!memberBilly McCafferty12 Jan '09 - 18:10 
It's been quite a while since I've written an IInterceptor and this did just the trick to jog my memory. Although this article is just a little out of date with NHibernate 1.2, it's still very helpful.
 
Thanks Ali!
Billy McCafferty
GeneralRe: Very helpful...thanks!memberAli Ozgur12 Jan '09 - 20:23 
Thank you and you are right that the article is out of date.
 
Some people ask me for help about IInterceptor implementation and if I can find some spare time I will try to provide another article about this subject. Another issue is that I did not implemented IInterceptor since the writing of this article although I extensively use NHibernate in some projects.
 

GeneralNew to NHibernate IInterceptor implementationmembervijay d5 Jan '09 - 4:13 
Hi,
 
I have recently started exploring the db logging through Interceptor. In one of my project i need to implement logging all the insert, update and delete events. I am trying to implement interceptor in my project first time.
can you please guide me in steps how do i implement interceptor for db logging? Can you please provide me a sample project if you have created of iinterceptor implemetation.
 
Thanks in advance,
Vijay
GeneralRe: New to NHibernate IInterceptor implementationmemberAli Ozgur12 Jan '09 - 20: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 classmemberJason Wilden23 May '08 - 23:24 
I'm sure you're aware of this but still might be worth highlighting.
 
There is an EmptyInterceptor class which implements the IInterceptor interface as virtual methods.
 
You can derive your Interceptor class from this and provide overrides for the methods that you actually want to provide with some functionality.
 
public class EntityInterceptor : EmptyInterceptor
{
        /// <summary>
        /// Called when a transient entity is passed to <c>SaveOrUpdate</c>.
        /// </summary>
        public override bool? IsTransient(object entity)        
        {
            // Do something in here...
        }
}

GeneralReuseable mapping componentmemberMBursill26 Jul '07 - 10:20 
First, great article! I'm playing around with NHibernate 2.2 and found this to be a huge help.
 
My approach on logging has been similar to what you describe. However, I'm wondering if there is a simpler way to do the mapping files?
 
So far I have created a class called AuditLog which contains in it the 4 properties. I have an interface called IAuditable that contains a property for AuditLog. Each business object implementing IAuditable will have an AuditLog property.
 
I can map the 4 properties contained in the AuditLog object via a component element in my mapping files.
 
What I would like though, is to make this even easier.
 
Is it possible (in some way) to create the component element in its own file, and than import it into each one of the XML mappings that require it? If not, I think I can define the class name of a mapping file to be IAuditable and than include the 4 properties, but how do I set the table? I want it to be as reusable as possible. Basically I want a near automated solution that will plug in/add these mappings for each class that implements IAuditable. That way, in the future, if I decide to audit something else (who knows what), I could simply add a property to my AuditLog class, and in theory one property element to the already existing component mapping element.
 
Is something like this even possible?
 
-Mike.
GeneralRe: Reuseable mapping componentmemberAli Ozgur26 Jul '07 - 20:53 
First of all i would like to thank you.
 
What I understand from your comment is that you want to inject mapping information automatically
to NHibernate mapping files ( in-memory representation of the mapping files).
 
I think NHibernate does not provide a simple way to do this. May be
 
1- By implementing User defined type for NHibernate you can minimize the number of fields you have to add to the mapping file. I mean by implementing a user defined type you can wrap all these 4 fields into a single type and refer in the mapping file only a single field with your user defined type.
 
2- You can modify embedded mapping file just before NHibernate loads mapped types with SessionFactory.AddAssembly(). You will have to utilize reflection and frankly i did not try if you can modify embedded resource in an assembly. In case you can not do this (because of .NET restrictions) you will propbaly have to follow these steps
- Get all types in the assembly
- Check if the type in hand implements your marker interface (IAuditable)
- Get the mapping file ( which is embedded as resource) out of the assembly
- Perform parsing to find out where you will inject you audit related field mappings
- Inject your audit related field mappings
- Pass NHibernate session factory's related function (if i am not wrong that was AddMapping) and make NHibernate load the class mapping from the modified mapping file
 
The second possibility can make you scratch your head because of type dependency issue but i think this is worth to give a try
 
NOTE: I did not tested any of the possibilities listed above. If you decide to try any of my suggestions I will be pleased to hear from you about the results.
 

 
Ali Ozgur
Software Developer
 
Turkey

QuestionWhat is up with the ? at the end of type fields?memberphilmee9514 May '07 - 11:16 
I am trying to figure nhibernate out, but the 3 articles I have looked at have the ? after the type (ex: public int? aNumber and
 
private DateTime? _sysCreatedOn = null;
public DateTime? SysCreatedOn
{
get { return _sysCreatedOn; }
set { _sysCreatedOn = value; }
}
 
I thought it was to maybe allow nulls without a comiler errors, but looking through ms help did not help. Is it an nhbernate syntax for something like column mapping or alias?
AnswerRe: What is up with the ? at the end of type fields?memberAli Ozgur14 May '07 - 20:14 
Hi,
 
This ? at the end of the type indicates that type is nullable.(This fetaure was introduced with .NET 2.0 ) Normally if you define a variable of type int you can not assign db null to this variable. Proiding the ? at the end of the type you can assign null to this variable.
 
private int nonNullableInt = null; //Compiler error.
private int? nonNullableInt = null; //That is ok.
 
Hint: Shortcut to test for null value. This is also new with .NET 2.0
 
private int? nullableInt = null;
 
if(( nullableInt ?? -1 ) == -1)
{
MessageBox.Show("Value is null")
}

 
Ali Ozgur
Software Developer
 
Turkey

GeneralRe: What is up with the ? at the end of type fields?memberphilmee9515 May '07 - 6:41 
Thank you. That should help a ton with dates, instead of a bunch of if null testing. Of course, I could have been using smartdate from the clsa too.
GeneralRe: What is up with the ? at the end of type fields?membervanslly26 Jul '11 - 9:09 
It's important to note that the ? in
int? nullableInt;
is syntactical sugar for
System.Nullable<int> nullableInt;
.

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

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