Complex NHibernate Auditing
Asked Answered
E

4

4

I'm using the IPostUpdateEventListener interface to do update audit logging now, grabbing the old and new values, then storing each updated field in an "Audit" table and all that jive. Works swell, but there's two last requirements I'm having a hard time fullfilling:

  1. Display which employee the update was for.
  2. Display the "friendly" name of the field updated.

For #1, my first instinct was to use reflection and look for & grab the "Employee" property on the given entity to find out which Employee it was for, but that quickly falls apart when you're a few objects deep in the graph with no automatic way to get back to the given Employee object.

Ideas to solve #1 ranged from requiring a "Parent" property on every object so I can traverse the graph looking for the Employee type (which, to me, would pollute our domain too much for a simple persistance concern) to using a separate SQL job to tranverse the foriegn keys and fill in the Employee ID after the fact (I'd rather not maintain a separate SQL job as everthing is code based thus far - and that SQL job will get quite nasty very quick).

As for the second requirement, I can get the actual property name that changed just fine. For a good 80% - 90% of our fields, the (properly formatted) property name is what we display, so I can just space the name based on the Pascal casing. The rest of the fields, however, don't match up for various reasons. We're using ASP.NET MVC and Fluent HTML builders from MvcContrib, but even if we modified the setup to the point of having an attribute on the view model that overridess what the field name should be (and therefor having it in code instead of just the view), there's no real way to match those attributes from the view models to the domain objects being saved.

A final pragmatic solution to both problems would just be to call an audit logging service after each update operation in another service, passing in the field names and employee information as needed, but, well, I really don't want to go there for obvious reasons.

Ideas for either problem would be greatly appreciated. Searching and racking my brain for a couple of days has turned up nothing of use - most people seem to stop at simple old/new vale recording or just a "created/updated" timestamp on the record itself.

Elsy answered 27/7, 2009 at 14:28 Comment(0)
A
3

I have a requirement similar to yours. In my case, it's a health care application and the audit log needs to identify the patient to which the insert/update applies.

My solution is to define an interface, which all audited classes need to implement :

public interface IAuditedRecord
{
    IPatient OwningPatient { get; }

    ...
    // Other audit-related properties (user, timestamp)
}

The audited classes then implement this interface in whatever way is required. For example:

public class Medication : IAuditedRecord
{
    // One end of a bidirectional association. Populated by NHibernate.
    private IPatient _patient;

    IPatient OwningPatient { get { return _patient; } }
}

public class MedicationNote : IAuditedRecord
{
    // One end of a bidirectional association. Populated by NHibernate.
    private Medication _medication;

    IPatient OwningPatient { get { return _medication.OwningPatient; } }
}

The IPostInsertEventListener and IPostUpdateEventListener then fetch the OwningPatient property in order to populate the audit record.

The solution has the advantages of keeping the auditing logic in the event listeners, which is the only place where one is sure that an insert/update will take place, as well as allowing the traversal of indirect links between an object and its owning patient.

The downside is that the audited classes have to derive from a specific interface. I think the benefits outweigh this small cost.

Asterism answered 3/8, 2009 at 11:59 Comment(2)
Interesting. I used a similar approach not long ago, except instead of a getter I had a setter on the IEntity interface, which each POCO implemented. I injected User on the constructor of the repository base class, and when a persist(IEntity poco) action was called on any repository class, it would call the setter on the persistent entity (we stored the last user to touch the record on the record itself). This allowed us to ignore event listeners altogether. The nice thing about your approach, however, is that you can pick and choose which classes should be audited.Caseation
Funny, that's pretty much what I ended up doing. I defined an "EmployeeIdForAuditing" property (only getter) on our IDomainEntity, then had it return null in the base DomainEntity class. We also had a requirement to know whether each entry was an Employee/Company level "edit", so we created an audit attribute with the information. Any object that was an IDomainEntity and tagged with the Employee level audit attribute is required to implement the property. This also allowed me to write a test to check all the objects in our project for those requirements. Thanks for posting this!Elsy
O
0

This seems like a more complicated scenario than a trivial audit trail, and for that reason I would lean towards an application service to handle this.

  1. Its much more testable.
  2. Its explicit.
  3. You don't have to resort to reflection or common properties.

We use an audit service in our application, even though our scenario is much simpler than yours because its a domain concern. Our domain knows about history and works with history, its not created just for some reporting front end.

Are you recording who (Employee) is making the update, or is Employee some sort of aggregate root?

Oysterman answered 27/7, 2009 at 20:30 Comment(2)
Employee is an aggregate root. We have a simple security related "user" making the change. Auditing is simply for record keeping and reporting in our system, though, and we never do anything with it, which is why I was hesitant to move it into a service. I had a feeling from the start of this that's where it'd have to end up due to technical reasons, unfortunately.Elsy
if you are using NHibernate as ORM, i am interested in how you are populating your history table unless you are using trigger. cheersOscular
S
0

For #1, I designed the following: In MyCommon assembly I declared

public interface IUserSessionStore
{
    UserSession Get();
    void Set(UserSession userSession);
}

which gets created per request using Ninject. The implementation of this class, simply stores the UserSession object in the ASPNET MVC Session.

With this, I can request the IUserSessionStore throughout all layers and get the UserSession for this class to use and to insert as audit info in every object (below class would be in MyModel project):

public class AuditUpdater : DefaultSaveOrUpdateEventListener
{
    private IUserSessionStore userSessionStore;
    public AuditUpdater(IUserSessionStore userSessionStore)
    {
        this.userSessionStore = userSessionStore;
    }

    private Guid GetUserId()
    {
        return userSessionStore.Get().UserId;
    }

    private void UpdateAuditCreate(IAuditCreate auditable)
    {
        if (auditable != null)
        {
            auditable.CreationDate = DateTime.UtcNow;
            auditable.CreatedBy = GetUserId();
        }
    }

    .......
}

So you can adapt this to obtain your Employee information that you need.

Gladly I'd take more suggestions!

Schnapps answered 6/5, 2014 at 15:52 Comment(0)
M
0

To create a log you can use IPostInsertEventListener or IPostUpdateEventListener. If you are using fluent configuration you has that configuring as in the example bellow.

The events will be called when have a post or update commit.

.ExposeConfiguration(c => c.EventListeners.PostCommitInsertEventListeners = new IPostInsertEventListener[] { new AuditEventPostInsert() })
.ExposeConfiguration(c => c.EventListeners.PostCommitUpdateEventListeners = new IPostUpdateEventListener[] { new AuditEventPostUpdate() });
Melancholia answered 3/5, 2016 at 22:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.