Simple Auditing Using an NHibernate IInterceptor (Part 2)
This is the second 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
- Part 1
- Part 2 (you are here)
- ……
- Part N
The Audit Interceptor Class
In order to create an Interceptor and hook into the NHibernate entity lifecycle, you have a few options. You can implement IInterceptor (NHibernate.IInterceptory) directly but I believe it is considered best practice instead to derive your class from EmptyInterceptor since the IInterceptor interface can (and will) change between versions.
1: public class AuditInterceptor : EmptyInterceptor { }
From this empty AuditInterceptor class you can override a number of methods from the EmptyInterceptor base class. We want to hook into create/update/delete events. Create and Delete are easy to find – you just override the OnSave() and OnDelete() methods. For updates you want the OnFlushDirty() method, which makes sense once you think about it.
So now our class looks like this:
1: public class AuditInterceptor : EmptyInterceptor
2: {
3: public override bool OnSave(object entity, object id, object[] state, string[] propertyNames, NHibernate.Type.IType[] types)
4: {
5: return base.OnSave(entity, id, state, propertyNames, types);
6: }
7:
8: public override bool OnFlushDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames, NHibernate.Type.IType[] types)
9: {
10: return base.OnFlushDirty(entity, id, currentState, previousState, propertyNames, types);
11: }
12:
13: public override void OnDelete(object entity, object id, object[] state, string[] propertyNames, NHibernate.Type.IType[] types)
14: {
15: base.OnDelete(entity, id, state, propertyNames, types);
16: }
17: }
Now we’ll create a general method for auditing object modifications that each of the above methods will call:
1: public void AuditObjectModification(object entity, object id, AuditActionType auditActionType)
2: {
3: throw new NotImplementedException();
4: }
So now you have the methods exposed – time to test for our expected behaviors.
Testing The Audit Interceptor
The Audit Interceptor has a constructor that takes two arguments: IUserAuth, which is a wrapper around the User.Identity.Name context, and IRepository<Audit> which provides strongly typed access into the NHibernate repository. These arguments (dependencies) are injected using Castle Windsor in production, but while testing will will provide stubs of them directly.
1: public AuditInterceptor AuditInterceptor { get; set; }
2:
3: public IUserAuth UserAuth { get; set; }
4: public IRepository<Audit> AuditRepository { get; set; }
5:
6: public AuditInterceptorTests()
7: {
8: UserAuth = MockRepository.GenerateStub<IUserAuth>();
9: AuditRepository = MockRepository.GenerateStub<IRepository<Audit>>();
10:
11: AuditInterceptor = new AuditInterceptor(UserAuth, AuditRepository);
12: }
Using Rhino Mocks to stub out the IUserAuth and IRepository<Audit> we can now begin testing the audit interceptor.
Tests:
*AuditObjectModificationSouldNotSaveAuditEntity – First we’ll test that the AuditObjectModification method will not save an audit object. If we did then it would create an infinite loop of self auditing and I’d rather not crash the web server. Basically we will just pass an audit object and make sure that the save method (repository.EnsurePersistent()) was not called.
1: [TestMethod]
2: public void AuditObjectModificationSouldNotSaveAuditEntity()
3: {
4: AuditInterceptor.UserAuth.Expect(a => a.CurrentUserName).Return("currentUser");
5:
6: AuditInterceptor.AuditObjectModification(new Audit(), null, AuditActionType.Update);
7:
8: AuditRepository.AssertWasNotCalled(a => a.EnsurePersistent(Arg<Audit>.Is.Anything));
9: }
*AuditObjectModificationSavesTheCurrentUser – Next we want to make sure that the current user is saved along with an audit entry.
1: [TestMethod]
2: public void AuditObjectModificationSavesTheCurrentUser()
3: {
4: Audit audit = null;
5:
6: AuditInterceptor.UserAuth.Expect(a => a.CurrentUserName).Return("currentUser");
7:
8: AuditRepository
9: .Expect(a => a.EnsurePersistent(Arg<Audit>.Is.Anything))
10: .WhenCalled(a => audit = (Audit)a.Arguments.First());
11:
12: AuditInterceptor.AuditObjectModification(new object(), null, AuditActionType.Update);
13:
14: Assert.AreEqual("currentUser", audit.Username);
15: }
*AuditObjectModificationLeavesObjectIdNull – Continuing along this path we now want the audit method to be able to handle null ids since newly created objects will not yet have ids.
1: [TestMethod]
2: public void AuditObjectModificationLeavesObjectIdNull()
3: {
4: Audit audit = null;
5:
6: AuditInterceptor.UserAuth.Expect(a => a.CurrentUserName).Return("currentUser");
7:
8: AuditRepository
9: .Expect(a => a.EnsurePersistent(Arg<Audit>.Is.Anything))
10: .WhenCalled(a => audit = (Audit)a.Arguments.First());
11:
12: AuditInterceptor.AuditObjectModification(new object(), null, AuditActionType.Update);
13:
14: Assert.IsNull(audit.ObjectId);
15: }
*AuditObjectModificationSetsObjectName – Now I want to make sure that the object passed in has its type saved in the audit class’s ObjectName property.
1: [TestMethod]
2: public void AuditObjectModificationSetsObjectName()
3: {
4: var sampleObject = new RouteConfigurator();
5:
6: Audit audit = null;
7:
8: AuditInterceptor.UserAuth.Expect(a => a.CurrentUserName).Return("currentUser");
9:
10: AuditRepository
11: .Expect(a => a.EnsurePersistent(Arg<Audit>.Is.Anything))
12: .WhenCalled(a => audit = (Audit)a.Arguments.First());
13:
14: AuditInterceptor.AuditObjectModification(sampleObject, null, AuditActionType.Update);
15:
16: Assert.AreEqual("RouteConfigurator", audit.ObjectName);
17: }
*AuditObjectModificationCallsEnsurePersistant – The last test will just make sure that when everything goes ok, a new audit instance will be saved.
1: [TestMethod]
2: public void AuditObjectModificationCallsEnsurePersistant()
3: {
4: AuditInterceptor.UserAuth.Expect(a => a.CurrentUserName).Return("currentUser");
5:
6: AuditRepository.Expect(a => a.EnsurePersistent(Arg<Audit>.Is.Anything));
7:
8: AuditInterceptor.AuditObjectModification(new object(), null, AuditActionType.Update);
9:
10: AuditRepository.AssertWasCalled(a=>a.EnsurePersistent(Arg<Audit>.Is.Anything));
11: }
Results
Of course since the AuditObjectModification method just throws a NotImplementedException(), all of these tests are going to currently fail.
So the next step is to implement AuditObjectModification() and get all of the tests to turn green. We’ll work on that next time!