James Pogran
James Pogran

Reputation: 4379

Entity Framework 4.1 DbContext Override SaveChanges to Audit Property Change

I am trying to implement a constrained 'audit log' of property changes to a properties in a set of classes. I have successfully found out how to set CreatedOn|ModifiedOn type properties, but am failing to find out how to 'find' the property that has been modified.

Example:

public class TestContext : DbContext
{
    public override int SaveChanges()
    {
        var utcNowAuditDate = DateTime.UtcNow;
        var changeSet = ChangeTracker.Entries<IAuditable>();
        if (changeSet != null)
            foreach (DbEntityEntry<IAuditable> dbEntityEntry in changeSet)
            {

                switch (dbEntityEntry.State)
                {
                    case EntityState.Added:
                        dbEntityEntry.Entity.CreatedOn = utcNowAuditDate;
                        dbEntityEntry.Entity.ModifiedOn = utcNowAuditDate;
                        break;
                    case EntityState.Modified:
                        dbEntityEntry.Entity.ModifiedOn = utcNowAuditDate;
                        //some way to access the name and value of property that changed here
                        var changedThing = SomeMethodHere(dbEntityEntry);
                        Log.WriteAudit("Entry: {0} Origianl :{1} New: {2}", changedThing.Name,
                                        changedThing.OrigianlValue, changedThing.NewValue)
                        break;
                }
            }
        return base.SaveChanges();
    }
}

So, is there a way to access the property that changed with this level of detail in EF 4.1 DbContext?

Upvotes: 39

Views: 30747

Answers (5)

Khurshid Rustamov
Khurshid Rustamov

Reputation: 9

See Using Entity Framework 4.1 DbContext Change Tracking for Audit Logging.

With DbEntityEntry. Detail audit for add, remove, modify

Upvotes: 0

Vland
Vland

Reputation: 4272

I really like Slauma's solution. I usually prefer to keep track of the modified table and records primary key(s) tho. This is a very simple method you can use to do that, calling getEntityKeys(entry)

    public static string getEntityKeys(ObjectStateEntry entry)
    {
        return string.Join(", ", entry.EntityKey.EntityKeyValues
                                .Select(x => x.Key + "=" + x.Value));
    }

Upvotes: 1

dan
dan

Reputation: 2388

It seems Slauma's answer does not audit changes to inner properties of a complex type property.

If this is a problem for you, my answer here might be helpful. If the property is a complex one and it has changed I serialise the whole complex property into the audit log record. It's not the most efficient solution but it's not too bad and it gets the job done.

Upvotes: 1

Slauma
Slauma

Reputation: 177163

Very, very rough idea:

foreach (var property in dbEntityEntry.Entity.GetType().GetProperties())
{
    DbPropertyEntry propertyEntry = dbEntityEntry.Property(property.Name);
    if (propertyEntry.IsModified)
    {
        Log.WriteAudit("Entry: {0} Original :{1} New: {2}", property.Name,
            propertyEntry.OriginalValue, propertyEntry.CurrentValue);
    }
}

I have no clue if this would really work in detail, but this is something I would try as a first step. Of course there could be more then one property which has changed, therefore the loop and perhaps multiple calls of WriteAudit.

The reflection stuff inside of SaveChanges could become a performance nightmare though.

Edit

Perhaps it is better to access the underlying ObjectContext. Then something like this is possible:

public class TestContext : DbContext
{
    public override int SaveChanges()
    {
        ChangeTracker.DetectChanges(); // Important!

        ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;

        List<ObjectStateEntry> objectStateEntryList =
            ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Added
                                                       | EntityState.Modified 
                                                       | EntityState.Deleted)
            .ToList();

       foreach (ObjectStateEntry entry in objectStateEntryList)
       {
           if (!entry.IsRelationship)
           {
               switch (entry.State)
               {
                   case EntityState.Added:
                       // write log...
                       break;
                   case EntityState.Deleted:
                       // write log...
                       break;
                   case EntityState.Modified:
                   {
                       foreach (string propertyName in
                                    entry.GetModifiedProperties())
                       {
                           DbDataRecord original = entry.OriginalValues;
                           string oldValue = original.GetValue(
                               original.GetOrdinal(propertyName))
                               .ToString();

                           CurrentValueRecord current = entry.CurrentValues;
                           string newValue = current.GetValue(
                               current.GetOrdinal(propertyName))
                               .ToString();

                           if (oldValue != newValue) // probably not necessary
                           {
                               Log.WriteAudit(
                                   "Entry: {0} Original :{1} New: {2}",
                                   entry.Entity.GetType().Name,
                                   oldValue, newValue);
                           }
                       }
                       break;
                   }
               }
           }
       }
       return base.SaveChanges();
    }
}

I've used this myself in EF 4.0. I cannot find a corresponding method to GetModifiedProperties (which is the key to avoid the reflection code) in the DbContext API.

Edit 2

Important: When working with POCO entities the code above needs to call DbContext.ChangeTracker.DetectChanges() at the beginning. The reason is that base.SaveChanges is called too late here (at the end of the method). base.SaveChanges calls DetectChanges internally, but because we want to analyze and log the changes before, we must call DetectChanges manually so that EF can find all modified properties and set the states in the change tracker correctly.

There are possible situations where the code can work without calling DetectChanges, for example if DbContext/DbSet methods like Add or Remove are used after the last property modifications are made since these methods also call DetectChanges internally. But if for instance an entity is just loaded from DB, a few properties are changed and then this derived SaveChanges is called, automatic change detection would not happen before base.SaveChanges, finally resulting in missing log entries for modified properties.

I've updated the code above accordingly.

Upvotes: 45

Jay
Jay

Reputation: 6294

You can use the methods Slauma suggests but instead of overriding the SaveChanges() method, you can handle the SavingChanges event for a much easier implementation.

Upvotes: 7

Related Questions