Adding Audit Capabilities to NHibernate Entities

by Shawn Duggan on 02/16/2009

NHibernate 2.0 brings many new features, including a new event / listener system that can be used for many kinds of cross-cutting concerns.  One the capabilities is being able to register a listener that can automatically update fields on an entity for you as it is saved.  Many business applications require that we track who last modified an entity, or when it was created etc.  This new model makes it quite easy to do.

In this example assume that in our project all our entities inherit from a generic base class called Entity.  The Entity class comes from the awesome S#arp Architecture project over on Google Code.  Even though it is developed as an ASP.NET MVC framework, I am using components of it in my WPF application and think its great.  If your ORM of choice is NHibernate, I highly recommend it.

Now back to our example, as most applications do not require auditing on all classes, we need a way to discriminate entities that we wish to add auditing to from those we do not.  We accomplish this through the use of an interface, IAuditableEntity:

   1: using System;
   2:
   3: namespace BlueTango.PrivatePractice.Core.DataInterfaces
   4: {
   5:     public interface IAuditableEntity
   6:     {
   7:         DateTime UpdatedDate { get; set; }
   8:         string UpdatedBy { get; set; }
   9:         DateTime CreatedDate { get; set; }
  10:         string CreatedBy { get; set; }
  11:         bool IsTransient();
  12:     }
  13: }

We can then derive from Entity to create an AuditableEntity class which implements IAuditableEntity:

   1: using System;
   2: using BlueTango.PrivatePractice.Core.DataInterfaces;
   3: using SharpArch.Core.DomainModel;
   4:
   5: namespace BlueTango.PrivatePractice.Core
   6: {
   7:     public class AuditableEntity : Entity, IAuditableEntity
   8:     {
   9:         public virtual bool IsActive { get; set; }
  10:
  11:         #region IAuditableEntity Members
  12:
  13:         public virtual DateTime UpdatedDate { get; set; }
  14:         public virtual string UpdatedBy { get; set; }
  15:         public virtual DateTime CreatedDate { get; set; }
  16:         public virtual string CreatedBy { get; set; }
  17:
  18:         #endregion
  19:     }
  20: }

The next step is to create our CustomSaveEventListener:

   1: using System;
   2: using BlueTango.PrivatePractice.Core.DataInterfaces;
   3: using NHibernate.Event;
   4: using NHibernate.Event.Default;
   5:
   6: namespace BlueTango.PrivatePractice.Data
   7: {
   8:     public class CustomSaveEventListener : DefaultSaveEventListener
   9:     {
  10:         protected override object PerformSaveOrUpdate(SaveOrUpdateEvent evt)
  11:         {
  12:             var entity = evt.Entity as IAuditableEntity;
  13:
  14:             if (entity != null)
  15:             {
  16:                 if (entity.IsTransient())
  17:                 {
  18:                     ProcessEntityBeforeInsert(entity);
  19:                 }
  20:                 else
  21:                 {
  22:                     ProcessEntityBeforeUpdate(entity);
  23:                 }
  24:             }
  25:
  26:             return base.PerformSaveOrUpdate(evt);
  27:         }
  28:
  29:         internal virtual void ProcessEntityBeforeInsert(IAuditableEntity entity)
  30:         {
  31:             entity.CreatedBy = SecurityHelper.GetCurrentUsername();
  32:             entity.CreatedDate = DateTime.Now;
  33:             entity.UpdatedDate = entity.CreatedDate;
  34:             entity.UpdatedBy = entity.CreatedBy;
  35:         }
  36:
  37:         internal virtual void ProcessEntityBeforeUpdate(IAuditableEntity entity)
  38:         {
  39:             entity.UpdatedBy = SecurityHelper.GetCurrentUsername();
  40:             entity.UpdatedDate = DateTime.Now;
  41:         }
  42:     }
  43: }

You can see in the code example above that the PerformSaveOrUpdate method determines if the object is an IAuditableEntity and then calls the appropriate save or update method.  SecurityHelper is just a simple class to help determine the current user:

   1: using System.Security.Principal;
   2: using System.Threading;
   3: using System.Web;
   4:
   5: namespace BlueTango.PrivatePractice.Data
   6: {
   7:     public class SecurityHelper
   8:     {
   9:         private static bool IsInWebContext()
  10:         {
  11:             return HttpContext.Current != null;
  12:         }
  13:
  14:         public static string GetCurrentUsername()
  15:         {
  16:             if (IsInWebContext())
  17:             {
  18:                 return Thread.CurrentPrincipal.Identity.Name;
  19:             }
  20:
  21:             var identity = WindowsIdentity.GetCurrent();
  22:             return identity != null ? identity.Name : string.Empty;
  23:         }
  24:     }
  25: }

My Core and Data layers are written to be reusable so this helper class should work in both web and windows applications.  The last step is to wire up NHibernate to use our new listener.  This is done through the config:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
   3:   <session-factory>
   4:     ...
   5:     <listener class="BlueTango.PrivatePractice.Data.CustomSaveEventListener, BlueTango.PrivatePractice.Data" type="save-update" />
   6:   </session-factory>
   7: </hibernate-configuration>
   8:

Something that used to be a chore is now done automatically for us.  Yet another reason why NHibernate is my ORM!

{ 13 comments… read them below or add one }

1 Tomas Matuschek February 27, 2009 at 9:04 am

I really enjoyed reading this, very helpful! I noticed your reference to the S#arp Architecture, and I would really like to know your experiences with it in a WPF environment? What parts are you using, and what to I need to stay away from? Any input would be highly appreciated. I’m looking to use parts of it to create an data-layer that can be used from WPF, WCF and potentially other environments. I used parts of Bill’s best practices from codeproject in a recent project and really liked it.

2 Shawn February 27, 2009 at 8:33 pm

Thanks Tomas. I really like S#arp, and in my current WPF project am using the Core, Data and Testing libraries. Obviously I don’t use the MVC bits. When it comes to NH Billy certainly knows his stuff, and its great that he’s created S#arp so that the rest of us can benefit from his knowledge. S#arp currently uses Windsor, but I’ve stuck with Ninject which older versions used. If you’re thinking about giving it a try you should go for it!

3 Chris jackson March 20, 2009 at 8:12 pm

Thank you very much for the article. The type of auditing that I’m trying to implement involves an audit table per entity along with a “current version” table per entity. Thus, for all updates, the previous state of the entity gets inserted into the audit table and the “current version” table gets updated. I’m curious if it’s possible to use this approach to handle this requirement.

4 Shawn March 21, 2009 at 8:44 am

A simpler approach might be to use triggers in your database to insert the original version of your entity into your audit table.

5 Trevor Westerdahl May 19, 2009 at 1:51 am

I posted a request on the NHibernate user forum. This should be really simple to implement, but its not supported as-is.

Nhibernate should support a Date/Time generator just like an ID generator. All that NHibernate needs to do is read which connection provider is in use and inject a function into the SQL (I.e. GetDate() for SQL Server) based on the current connection.

For mapping, NHibernate just needs to have the field identified that should be generated (I.e. let NH it know to inject the function) and it should know the behavior (I.e. inject only on insert/new or always). How hard of a mod is that?

In my opinion, it is not appropriate for the date/times to be set outside the scope of the database transaction (they are inaccurate) and I, personally, do not favor triggers or stored procedures for this solution.

I should be able to create a mapping and have NH auto-create the schema and auto-handle injecting the function in the SQL.

6 Aaron Dixon May 21, 2009 at 4:45 pm

I agree with Trevor on this one. I see the usefulness of Listeners and Interceptors but the scenario of handling Audit fields should be handled with simple mapping.

Currently it seems that if you want to update/insert auditing info you need the listener model. I do however believe that if you are going to use an actual auditing table where you copy an entire row to another table you should be using triggers.

7 isuruceanu February 11, 2010 at 7:08 am

I did the same things but I get and stackoverflow exception. It enters recursively in the PerformSaveOrUpdate overrided method. After I’m setting createdBy it call PerformSaveOrUpdate one more time, and logically enters into recursion.

The only difference from yours code is that CreatedBy and ModifiedBy are User object and Mapped to Users table as Reference.

Have you such problem? Could you change a bit you code and test with references to User instead of having string, please?

8 Shawn Duggan February 11, 2010 at 9:38 pm

@isuruceanu The application that sample came from is in production with virtually little change from that sample. No doubt its the mapping to your User object. Sorry but I won’t be able to change my code as you ask due to time constraints. Something for you to think about though – why do you need a full User object in an audit field? Why not just the username or userId?

9 isuruceanu February 15, 2010 at 4:52 am

After few days of investigation I found that the problem is on the entities with hierarchical data, e.q. A Message is an auditable one and it has a list of receiver. On Saving Message it tries to save cascading all children objects
(NHibernate.Engine.CascadingAction.SaveUpdateCascadingAction.Cascade)

And it seams that when I require GetLoggetUser() methods, Nhibernate tries to solve all resistances, finally it calls SaveOrUpdate cascading and enters in infinite loop.

PS: I like your question, I’ll try to change DOs.

10 RG April 20, 2010 at 8:04 pm

Im currently working on an MVC project with NHibernate and am using your auditing capabilities however I’m having one issue that I’m hopefuly you can help me solve.

The problem is that when I do a save on an object that has child entities associated (which implement IAuditableEntity), these entities have their UpdatedDate, UpdatedBy etc updated causing NHibernate to save all entities in the object graph, even though child entities have not been modified. This is a big issue when the object graph is large.

Would you know of any solution to this?

Your help would be greatly appreciated.

11 Shawn Duggan April 22, 2010 at 9:03 am

@RG / @isuruceanu,

One suggestion I have is to extend the IAuditableEntity to include a dirty flag – IsDirty. This will then be implemented on all your auditable entities. When you implement INotifyPropertyChanged on each entity, you should also trip the IsDirty flag when something is changed.

In the CustomSaveEventListener you can then check to make sure the entity is dirty before doing any of the processing.

Let me know if this works for you.

12 Nhibernate debutant.... August 25, 2010 at 11:39 am

Hi,

Thank you for the post.
Could you please let me know where is the implementation of
bool IsTransient(); in AuditableEntity class??? I am not sure how I will determine where entity is transient or not. I understand nhibernate determines on session save mthod.

Regards,
Nhibernate debutant….

13 Shawn Duggan August 30, 2010 at 10:55 am

It is implemented by the SharpArch.Core.Entity class, which my AuditableEntity inherits from.

Leave a Comment

Previous post:

Next post: