War
War

Reputation: 8628

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"
        },
        ...
      ]
    }

Upvotes: 1

Views: 1697

Answers (3)

Jonathan Magnan
Jonathan Magnan

Reputation: 11337

You need to keep the ObjectStateEntry in your AuditEntry then revisit every AuditEntry which the primary key was "Temporary" in a PostSaveChanges event.

Here is an example:

Obviously I recommend you to use EF+ Audit over creating your own library but if you still want to code it, the library is open source so you will be able to find a lot of information to help you.

Disclaimer: I'm the owner of the project EF+ (EntityFramework Plus)

Upvotes: 1

War
War

Reputation: 8628

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.

Upvotes: 0

JotaBe
JotaBe

Reputation: 39015

As far as I know there are no events to intercetp object creation, after the object has been created. There are events for:

The first happens before the changes are saved (same problem you have). The second one happens when an entity is read from the database as result of a query or .Load, so it doesn't fit your case.

The only solution I can think of is that you override the original SaveChanges, and do this in the overridden method:

  • look for all the entities in the Added state, and keep references to them, for example adding them in a List<Object>
  • call the base save changes, so that the changes are materialized in the database, and the entities in the DbCOntext get updated
  • access the entities in the list, and you'll get all the db generated properties (like calculated, identities, guid and so on), so that you can log them correctly

The Logging and Intercepting Database Operations (EF6 Onwards) doesn't fit your needs

Upvotes: 1

Related Questions