Try fast search NHibernate

13 May 2011

NHibernate: The bizarre Audit

This is another post tagged as “wasting time”; in this occasion thanks to Scott Findlater and Filip Kinský (@Buthrakaur).

The title is because there are people who think that having four properties to store a DateTime of an entity creation, the User who have created it, the DateTime of the last modification and the User who have modified it mean that his application has auditing.

If you are a NHibernate’s user you know that we are providing many events to catch and even override NHibernate behavior. As I said long time ago, just after implement all those events/listeners, "what you can do with writing or overriding NHibernate default events is limited only by your imagination" (sorry to be auto-referential).

In the NET you can find various examples about how implement a simple “audit” using NHibernate. Most of those implementations come from some very old JAVA examples based on IPreInsertEventListener and IPreUpdateEventListener. Are those examples completely wrong ? No, they aren’t wrong!! They are just some simple examples that may work or may not work in your case. Scott (Findlater) have pointed me to the fact that even in the “NHibernate 3.0 cookbook” there is an example using IPreInsertEventListener and IPreUpdateEventListener and that is true… as true is that there is another example without using NH’s events at all (page 244) and there is the full list of NH’s events (page 238).

IPreInsertEventListener and IPreUpdateEventListener

If you look inside NHibernate, you can see that NHibernate does not have a default implementation of those two events and this mean that, for NHibernate, those two events are there just for your usage (in NH nothing is less important than an interface without implementation). You can do various things with those events as just log changes (NHibernate 3.0 cookbook page 235) or vetoing the action (as we are doing in NHibernate.Validator). Can you change the internal state maintained by NHibernate ? yes because what you can do “is limited only by your imagination”; does it mean that NHibernate will follow your imagination even when you have no idea what mean its internal state and how work with it ? No, it just mean that your imagination about how work NHibernate may fail.

Your anxiety will not change the passing of events

What you should do if NHibernate does not follow your imagination ?

Well… the last thing to do is bore me with +20 public posts just to then discover that instead apply my solution you have interpreted it, and you have introduced a bug in your code, or write a WIKI (that is the abbreviation of “What I Know Is”) just to say that you don’t know why your issue was closed as “Duplicated” of an issue closed as “Not an issue” where the first user, after an explication with code included, has recognized that his interpretation had a bug. Instead this “little boy” behavior you can ask for help in the nhusers group then, perhaps, you can write a simple solution with some tests and where nobody can help you, you may send a mail to me asking, with a nice “please”, help for free… perhaps I’ll write a public blog post with your problem and the solution… perhaps.

The pour auditing listener

Some entities may implements this interface:

public interface ITrackModificationDate
{
    DateTime LastModified { get; set; }
}

Filip have published his domain and his test here, then he sent me a zip. I took his tests and I have added some more tests finding some bugs. The pour auditing listener passing all tests is this:

public class SetModificationTimeEventListener : IFlushEntityEventListener, ISaveOrUpdateEventListener, IMergeEventListener
{
    // WARNING : if you need to log dirty properties the work to do is another.

    public SetModificationTimeEventListener()
    {
        CurrentDateTimeProvider = () => DateTime.Now;
    }

    public Func<DateTime> CurrentDateTimeProvider { get; set; }

    public void OnFlushEntity(FlushEntityEvent @event)
    {
        var entity = @event.Entity;
        var entityEntry = @event.EntityEntry;

        if (entityEntry.Status == Status.Deleted)
        {
            return;
        }
        var trackable = entity as ITrackModificationDate;
        if (trackable == null)
        {
            return;
        }
        if (HasDirtyProperties(@event))
        {
            trackable.LastModified = CurrentDateTimeProvider();
        }
    }

    private bool HasDirtyProperties(FlushEntityEvent @event)
    {
        ISessionImplementor session = @event.Session;
        EntityEntry entry = @event.EntityEntry;
        var entity = @event.Entity;
        if(!entry.RequiresDirtyCheck(entity) || !entry.ExistsInDatabase || entry.LoadedState == null)
        {
            return false;
        }
        IEntityPersister persister = entry.Persister;

        object[] currentState = persister.GetPropertyValues(entity, session.EntityMode); ;
        object[] loadedState = entry.LoadedState;

        return persister.EntityMetamodel.Properties
            .Where((property, i) => !LazyPropertyInitializer.UnfetchedProperty.Equals(currentState[i]) && property.Type.IsDirty(loadedState[i], currentState[i], session))
            .Any();
    }

    public void OnSaveOrUpdate(SaveOrUpdateEvent @event)
    {
        ExplicitUpdateCall(@event.Entity as ITrackModificationDate);
    }

    public void OnMerge(MergeEvent @event)
    {
        ExplicitUpdateCall(@event.Entity as ITrackModificationDate);
    }

    public void OnMerge(MergeEvent @event, IDictionary copiedAlready)
    {
        ExplicitUpdateCall(@event.Entity as ITrackModificationDate);
    }

    private void ExplicitUpdateCall(ITrackModificationDate trackable)
    {
        if (trackable == null)
        {
            return;
        }
        trackable.LastModified = CurrentDateTimeProvider();
    }
}

This is not indented to be THE SOLUTION of every kind of similar situation. A similar situation may have a similar solution. This is the solution to pass some tests provided by Filip Kinský, that’s all.

The advanced Audit solution

(This section is dedicated to the master AJL to prevent his question)

If the above is a “pour Audit” which is the “rich Audit” ?

The rich audit using NHibernate was started by Simon Duduica, finished by Roger Kratz with some very little touch implemented by me.

The name of the most powerful audit system for NHibernate is: NHibernate.Envers

The code of NHibernate.Envers is in bitbucket.org : https://bitbucket.org/RogerKratz/nhibernate.envers

A presentation of its power is available in Spanish : http://www.altnethispano.org/wiki/van-2011-02-26-audit-parallel-model-con-nhibernate-3.ashx

What I should do if I want a “rich audit” without use NHibernate.Envers ?

You should study NHibernate.Envers and make it a little bit better, not worst, and then share your code.

Conclusion

1) Your anxiety will not change the passing of events.

2) when I say you that your issue is not a bug try to do everything possible to explain your case instead insist in your position.

3) If I send you some code, first use it as is, then run your tests, and only then try to make it “more elegant“ (red, green, think, refactor).

Post scriptum

I forgot to share the full configuration of NHibernate:

[TestFixtureSetUp]
public void BuildSessionFactory()
{
    listener =    new SetModificationTimeEventListener()
        {
            CurrentDateTimeProvider = () => defaultDate
        };

    var config = new Configuration();
    config.DataBaseIntegration(x =>
                               {
                                                             x.Dialect<MsSql2008Dialect>();
                                                             x.ConnectionStringName= "LocalForTests";
                                                             x.SchemaAction = SchemaAutoAction.Recreate;
                               });

    // Full mapping registration
    var mapper = new ConventionModelMapper();
    mapper.Class<Thing>(rcm => rcm.Bag(x => x.RelatedThings, map =>
                                                             {
                                                                                                                         map.Key(km=> km.OnDelete(OnDeleteAction.Cascade));
                                                                                                                         map.Cascade(Cascade.All.Include(Cascade.DeleteOrphans));
                                                                                                                         map.Inverse(true);
                                                             }));
    var mappings = mapper.CompileMappingFor(new[] { typeof(Thing), typeof(InheritedThing), typeof(RelatedThing) });
    config.AddDeserializedMapping(mappings,"HisDomain");

    // Events Liseners registration
    config.EventListeners.SaveEventListeners = new[] { listener }.Concat(config.EventListeners.SaveEventListeners).ToArray();
    config.EventListeners.SaveOrUpdateEventListeners = new[] { listener }.Concat(config.EventListeners.SaveOrUpdateEventListeners).ToArray();
    config.EventListeners.UpdateEventListeners = new[] { listener }.Concat(config.EventListeners.UpdateEventListeners).ToArray();
    config.EventListeners.MergeEventListeners = new[] { listener }.Concat(config.EventListeners.MergeEventListeners).ToArray();
    config.EventListeners.FlushEntityEventListeners = new[] { listener }.Concat(config.EventListeners.FlushEntityEventListeners).ToArray();

    sessionFactory = config.BuildSessionFactory();
}

that is NHibernate 3.2 sexy mapping Winking smile