Search code examples
c#entity-frameworkentityaudit-logging

EF Auditing Creations


Ok there's a lot going on here and I don't want to bore you guys with a very long winded code sample so here's an extract ...

When SaveChangesAsync() is called on my EF context I have it calling this method to audit each entry ...

async Task Audit(DbEntityEntry<IAmAuditable> entry)
{
    try
    {
        var newAuditEntry = new AuditEntry
        {
            EntityType = entry.Entity.GetType().Name,
            Event = entry.State.ToString(),
            SSOUserId = kernel.Get<User>().Id,
            EntityId = entry.GetId().ToString(),
            EventId = eventId
        };

In the event that the entry in question is an entity creation which will result in an insert on the db i then also do this ...

var properties = entry.CurrentValues.PropertyNames.Select(p => entry.Property(p)).ToList();
var addedValues = new List<AuditDataItem>();

foreach (var p in properties)
{
    addedValues.Add(new AuditDataItem
    {
        PropertyName = p.Name,
        PreviousValue = null,
        NewValue = p.CurrentValue.ToString()
    });
}
newAuditEntry.Changes = addedValues;
break;

... this is where it falls over ... at that point in time the base call to SaveChanges hasn't yet been executed so the entity in question does not yet have a primary key value ... the net result is that I log the creation of an entity with no primary.

Does anyone have suggestions on a nice clean way to handle this so I can put the new primary key value in to an AuditDataItem?

EDIT:

Here's an example of what I am logging at the moment as json, this is a single AuditEntry object and a partial of some of the child AuditDataItem rows ...

   {
      "Id": 4,
      "SSOUserId": 1,
      "EventId": "6d862aad-0898-4794-aea0-00af6f2994ff",
      "EntityType": "AC_Programme",
      "Event": "Added",
      "TimeOfEvent": "2016-02-04T12:04:31.5501508+01:00",
      "Changes": [
        {
          "Id": 34,
          "PropertyName": "Id",
          "PreviousValue": null,
          "NewValue": "0"
        },
        {
          "Id": 35,
          "PropertyName": "Name",
          "PreviousValue": null,
          "NewValue": "Test"
        },
        ...
      ]
    }

Solution

  • Ok so here's what i came up with ... curious to know what you guys think ...

    public override async Task<int> SaveChangesAsync()
    {
        try
        {
            await AuditChanges(new[] { EntityState.Modified, EntityState.Deleted });
            var result = await base.SaveChangesAsync();
            await AuditChanges(new[] { EntityState.Added });
            return result;
        }
        catch (DbEntityValidationException ex) { throw ConstructDetailsFor(ex); }
    }
    
    async Task AuditChanges(EntityState[] states)
    {
        var auditableEntities = ChangeTracker.Entries<IAmAuditable>()
            .Where(e => states.Contains(e.State));
    
        foreach (var entry in auditableEntities)
            await Audit(entry);
    }
    
    async Task Audit(DbEntityEntry<IAmAuditable> entry)
    {
        ...
    

    This is about as simple as it gets :)

    My audit method then basically enters in to a switch statement and decides what logic to run based on the passed in auditable entity entry from the change tracker.

    I don't think auditing could get much simpler than this.

    I put this in to a generic base class for all my EF contexts and run a migration to apply this to all db's and bang ... everywhere gets dynamic, auto auditing on all entities that are marked with "IAmAuditable" (an empty marker interface).

    I thought about using an attribute but that would require reflection and what not.