NHibernate Interceptors

by August 23, 2009 02:32 PM

A long time ago I asked a question on stackoverflow about table update events. The title of the question didn't really do it justice - I called it that because I assumed there would be events I could subscribe to in that would let me know when nhibernate was about to perform a database operation. In the end I figured out that what I was after is called an Interceptor.

The business case is simple: there is some data considered important enough we want to know everything that happens to the data. Classic auditing situation. The solution to these auditing needs however wasn't your standard single audit table. Instead, for any table containing "auditable" data, there is a sister table suffixed with "_HIST" that contains every column in the master table plus three additional - user, date, and action (Insert, Update, or Delete).

So when I joined the team and eventually introduced NHibernate, I started looking for a slick way to handle the auditing needs with NHibernate. My hope was that I could do something in the mappings and thus not have to change the domain entities themselves in any way. Unfortunately I never found something that would allow that, so the below is the best thing I could think of.

First, create an interface to implement on any entities who have auditing needs. This interface will not only tell the NHibernate code I show later that this is an entity to audit, but will also return the auditor that will do the work.

public interface IEntityToAudit
{
IAuditor Auditor { get; }
}
public interface IAuditor
{
void AuditInsert();
void AuditUpdate(object[] previousState, string[] propertyNames);
void AuditDelete();
}

The reason I'm passing in the previous state on update is because one of our entities only audits a few of the columns in the table it maps to. This means every time an update happens, I have to check those fields specifically to see if I need to insert a row in the audit table.

With that in place, all I have to do on my entities is implement the interface on any objects that map to a table we have to audit. For example:

public class MinorLine : IEntityToAudit
{
protected MinorLine()
{
Auditor = new MinorLineAuditor(this);
}
public MinorLine(string code, int costCenter) : this()
{
Code = code;
CostCenter = costCenter;
}
public virtual IAuditor Auditor { get; private set; }
public virtual int Id { get; protected set; }
public virtual string Code { get; protected set; }
public virtual int CostCenter { get; set; }
}

You might have noticed that we have to specify the actual implementation of the auditor here. This was for two reasons. First, I didn't want the NHibernate code to have to map from an object to all of the specific types that can be audited. Second, creating it here allowed me to pass in the entity under audit to the auditor's constructor, thus saving me from having to deal with the untyped data that I have in the NHibernate code.

The auditor itself unfortunately just does raw ADO.NET stuff.  When asked to do an AuditInsert, it inserts a new row into its audit table with an action of 'Insert'.  When asked to do an AuditUpdate, it does the same insert, but with an action of 'Update'. Each of my auditors have one method that does the actual insert. The three methods on the interface just delegate to that method passing in the action. So below, you can see _minorLine is the entity that was passed into the constructor and actionId is the method parameter specifying which action we're auditing.

cmd.CommandText =
"INSERT INTO MINOR_LINE_MASTER_HIST " +
"(                                  " +
"   MINOR_LINE_ID,                  " +
"   MINOR_LINE_CODE,                " +
"   COST_CENTER_NUM,                " +
"   ACTN_USERID,                    " +
"   ACTN_ID,                        " +
"   ACTN_DATE                       " +
")                                  " +
"VALUES                             " +
"(                                  " +
"   :MINOR_LINE_ID,                 " +
"   :MINOR_LINE_CODE,               " +
"   :COST_CENTER_NUM,               " +
"   :ACTN_USERID,                   " +
"   :ACTN_ID,                       " +
"   SYSTIMESTAMP                    " +
")                                  ";
DbHelper.AddInParameter(cmd, "MINOR_LINE_ID", _minorLine.Id);
DbHelper.AddInParameter(cmd, "MINOR_LINE_CODE", _minorLine.Code);
DbHelper.AddInParameter(cmd, "COST_CENTER_NUM", _minorLine.CostCenter);
DbHelper.AddInParameter(cmd, "ACTN_USERID", Username());
DbHelper.AddInParameter(cmd, "ACTN_ID", actionId);

At this point, I've shown you how I flag my entities as an entity to audit and how I handle the actual auditing. The only thing left is to wire up the code that calls the auditors at the right times. I'm using Fluent NHibernate for both the mappings and the configuration. In the configuration is where you tie together all this magic. Specifically, notice the call to "SetInterceptor"

_sessionFactory = Fluently.Configure()
.Database(OracleDataClientConfiguration
.Oracle9
.ConnectionString(c => c.FromConnectionStringWithKey("CPSDsn"))
.Driver("NHibernate.Driver.OracleClientDriver")
.ShowSql()
)
.Mappings(mapping => mapping.FluentMappings.AddFromAssemblyOf<Repository>())
.ExposeConfiguration(config => config.SetInterceptor(new AppInterceptor()))
.BuildSessionFactory();

And the AppInterceptor:

public class AppInterceptor : EmptyInterceptor
{
public override bool OnSave(object entity, object id, object[] state, string[] propertyNames, IType[] types)
{
var entityToAudit = entity as IEntityToAudit;
if (entityToAudit != null)
entityToAudit.Auditor.AuditInsert();
return base.OnSave(entity, id, state, propertyNames, types);
}
public override bool OnFlushDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types)
{
var entityToAudit = entity as IEntityToAudit;
if (entityToAudit != null)
entityToAudit.Auditor.AuditUpdate(previousState, propertyNames);
return base.OnFlushDirty(entity, id, currentState, previousState, propertyNames, types);
}
public override void OnDelete(object entity, object id, object[] state, string[] propertyNames, IType[] types)
{
var entityToAudit = entity as IEntityToAudit;
if (entityToAudit != null)
entityToAudit.Auditor.AuditDelete();
base.OnDelete(entity, id, state, propertyNames, types);
}
}

And that's it! Once in place, I've found that adding new entities to the domain with auditing needs is extremely easy. Just create the entity and do the mappings like you've always done. Then when you're ready, just implement the interface and the auditor and you're done.

Tags: ,

Comments (2) -

Toran Billups
Toran Billups
8/23/2009 8:35:39 PM #

Rob - love the solution! If you could clarify one area it would be around the implementation for the object that implements IAuditor.  The cmd.CommandText = "..." has me thinking "what now" - but then again I'm new to NHibernate Tong

Rob
Rob
8/23/2009 10:33:36 PM #

Thanks Toran. Note where I said in the post, "raw ADO.NET stuff." Basically inside our auditors you'd see old school data access. So however you prefer to do that is what you'd do here. No NHibernate goodness at all unfortunately. The 'cmd' object that you see is just a plain old DBCommand instance.

Keep in mind that's just the way we preferred to do it. I suppose you could create a whole object model around the history tables and map those with NHibernate. We chose the raw ADO.NET route since we're just doing inserts and never querying or doing anything with the data in the history tables.

Also keep in mind that I'm also new to NHibernate (VERY new) and it's entirely possible that there's a much more elegant solution out there. I just couldn't find one and needed to come up with something quickly and move on.

Hope that helps!

Comments are closed