Simple auditing using an NHibernate IInterceptor (Part 4)
This is the fourth and final post of a multi-part post series on writing simple auditing functionality for an ASP.NET application using NHibernate. The requirement was that every object modification event in the system should be logged by username and date. Specifically I don’t need to know exactly which properties were changed (just that a user was updated by whom at what time), but if you do need to save the changed properties there are plenty of hooks to do that.
I’ll update this post with links to the next parts when they become available
Hooking up the NHibernate IInterceptor
Now that we have written our IInterceptor, implemented as the AuditInterceptor class, we need to tell NHibernate to use it. This part will vary depending on how NHibernate is exposed to your application, but the basic idea is that when you open a session (using OpenSession() on your NHibernate session factory) you will pass along the interceptor as a parameter. If you have no interceptor, you can just call OpenSession().
Extending the NHibernate Session Manager
Our session manager class follows loosely with the thread-safe, lazy singleton patterned NHibernateSessionManager described in the NHibernate Best Practices Article. Adding hooks for a persistent interceptor was pretty easy – first I wrote a method for registering an interceptor:
1: /// <summary>
2: /// Allows you to set the interceptor which will be used in all subsequent sessions
3: /// </summary>
4: public void RegisterInterceptor(IInterceptor interceptor)
5: {
6: Check.Require(interceptor != null, "interceptor may not be null");
7:
8: _registeredInterceptor = interceptor;
9: }
This just sets a local static IInterceptor property called _registeredInterceptor. This is used whenever opening a session, as follows:
1: /// <summary>
2: /// Shortcut which just calls into the GetSession overload with the current registered interceptor.
3: /// If no interceptor has been registered, it will be null
4: /// </summary>
5: public ISession GetSession()
6: {
7: return GetSession(_registeredInterceptor);
8: }
9:
10: /// <summary>
11: /// Gets a session with or without an interceptor. This method is not called directly; instead,
12: /// it gets invoked from GetSession.
13: /// </summary>
14: private ISession GetSession(IInterceptor interceptor)
15: {
16: ISession session = ThreadSession;
17:
18: if (session == null)
19: {
20: session = interceptor != null ? sessionFactory.OpenSession(interceptor) : sessionFactory.OpenSession();
21:
22: ThreadSession = session;
23: }
24:
25: Check.Ensure(session != null, "session was null");
26:
27: return session;
28: }
So once the IInterceptor is registered, all future calls to GetSession() will open a session using that interceptor (of course you could null it out again later, though I am not sure why you would want to).
Registering the Interceptor
On Application_Start in the global.asax file I call into the NHibernateSessionManager’s RegisterInterceptor method (seen above). The interceptor I want to register will be resolved by Castle Windsor through IoC/DI.
1: //Configure the audit interceptor
2: NHibernateSessionManager.Instance.RegisterInterceptor(windsorContainer.Resolve<IInterceptor>());
To tell Castle Windsor to use the AuditInterceptor, we add the following line to the IoC container registration:
1: container.AddComponent("auditInterceptor", typeof (NHibernate.IInterceptor), typeof (AuditInterceptor));
Trying It Out
We are going to add a new “project” instance to our database. Basically in the code ASP.NET MVC creates a new Project instance with a Name property set, and makes sure it is Active by default. The code is kind of beside the point but I’ll post it anyway just make clear how transparent this interceptor is:
1: [AcceptPost]
2: public ActionResult CreateProject(Project newProject)
3: {
4: CreateEntity<Project, int>(newProject, "Project");
5:
6: return this.RedirectToAction(a => a.Projects());
7: }
8:
9: private void CreateEntity<T, IdT>(T entity, string type) where T : DomainObject<T, IdT>
10: {
11: var lookupEntity = entity as LookupObject<T, IdT>; // If this is a lookup entity we want to make sure isActive is true
12:
13: if (lookupEntity != null) lookupEntity.IsActive = true;
14:
15: ValidationHelper<T>.Validate(entity, ModelState);
16:
17: if (!ModelState.IsValid)
18: {
19: return;
20: }
21:
22: //Add the new entity
23: Repository.OfType<T>().EnsurePersistent(entity);
24: }
Let’s see (using NH Profiler) what happened to the database:
Perfect! We have two Insert Into statements wrapped in a transaction. The first statement saves the Project (I didn’t show it, but it is a simple Insert Into Projects with name = “NewProject”), and then second statement saves the audit record. You can see in the Details tab below that the record shows a Project was created by ‘srkirkland’ and saves the exact time of the action.
An update an delete look pretty much the same – here’s a quick screenshot of an update record being audited:
Ok – everything looks good. In the last four posts we have created a fully transparent auditing solution for recording all create/update/delete actions without our application. Pretty powerful stuff!