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

10 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. you're twisting the facts and simplifying little bit, but never mind.. thanks for the post, because this is the only proper solution (or at least I hope so :)) to this simple problem contrary to posts like http://ayende.com/Blog/archive/2009/04/29/nhibernate-ipreupdateeventlistener-amp-ipreinserteventlistener.aspx which suffer from serious bugs..

    ReplyDelete
  3. Could you provide the additional tests you mentioned to get insight what's wrong with my original solution, please?

    ReplyDelete
  4. NHibernate: The bizarre arrogance - http://www.scottfindlater.co.uk/blog/nhibernate-the-bizarre-arrogance Some food for thought :)

    ReplyDelete
  5. NHibernate: The bizarre arrogance http://www.scottfindlater.co.uk/blog/nhibernate-the-bizarre-arrogance

    ReplyDelete
  6. Any thoughts to something like this for adding listeners:

    config.DataBaseIntegration(x =>
    {
    x.Dialect();
    x.ConnectionStringName= "LocalForTests";
    x.SchemaAction = SchemaAutoAction.Recreate;
    }).Listeners(x => {
    x.AppendListeners........
    });

    Would be nice to not have to split up the declaration and just provide it as 1 block.

    Just my 2c

    ReplyDelete
  7. Hi there! Keep it up! This is a good read. I will be looking forward to visit your page again and for your other posts as well. Thank you for sharing your thoughts about rcm solutions in your area. I am glad to stop by your site and know more about rcm solutions .
    The important functions (of a piece of equipment) to preserve with routine maintenance are identified, their dominant failure modes and causes determined and the consequences of failure ascertained. Levels of criticality are assigned to the consequences of failure. Some functions are not critical and are left to "run to failure" while other functions must be preserved at all cost. Maintenance tasks are selected that address the dominant failure causes. This process directly addresses maintenance preventable failures. Failures caused by unlikely events, non-predictable acts of nature, etc. will usually receive no action provided their risk (combination of severity and frequency) is trivial (or at least tolerable). When the risk of such failures is very high, RCM encourages (and sometimes mandates) the user to consider changing something which will reduce the risk to a tolerable level.
    The RCM Blitz™ Solutions process was put together to allow companies to complete a traditional RCM analysis on a major critical asset by defining the envelope, completing the upfront tasks, and then setting up your meeting to complete the analysis in less than 5 days.

    ReplyDelete
  8. This is WAY easier to use than NHibernate:
    https://www.kellermansoftware.com/p-47-net-data-access-layer.aspx

    ReplyDelete
  9. @Asava Samuel, so cool! NHibernate = $0, Knight DAL = u$s99 p/dev HAHAHA, if you want to advertise your product, use google adwords!

    ReplyDelete
  10. Excellent Post. Also visit http://msnetframework.com/#mono.php

    ReplyDelete