60

Is there a way to get all the changes made to a object in the Entity Framework before it saves all changes. The reason for this is that i want to create a log table in our clients database:

so...

Is there a way to get the current database values(old) and the new values(current) before changes are saved?

If not, how can i achieve this in a generic way, so all my View Models can inherit from this?(I am using the MVVM + M Structure)

Willem
  • 9,166
  • 17
  • 68
  • 92

4 Answers4

73

You can use ObjectContext's ObjectStateManager,GetObjectStateEntry to get an object's ObjectStateEntry, which holds its original and current values in the OriginalValues and CurrentValues properties. You can get the names of the properties that changed using the GetModifiedProperties method.

You can write something like:

var myObjectState=myContext.ObjectStateManager.GetObjectStateEntry(myObject);
var modifiedProperties=myObjectState.GetModifiedProperties();
foreach(var propName in modifiedProperties)
{
    Console.WriteLine("Property {0} changed from {1} to {2}", 
         propName,
         myObjectState.OriginalValues[propName],
         myObjectState.CurrentValues[propName]);
}
danielr
  • 130
  • 1
  • 8
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • 2
    Have a look at this article [Audit Trail Using EF ObjectContext](http://www.codeproject.com/Articles/34491/Implementing-Audit-Trail-using-Entity-Framework-Pa) – Vishnoo Rath Apr 29 '13 at 12:29
  • He is using the same methods as well – Panagiotis Kanavos May 01 '13 at 13:59
  • 'ApplicationDbContext' does not contain a definition for 'ObjectStateManager' – trees_are_great May 11 '17 at 14:52
  • @Sam there is no `ApplicationDbContext` class int EF. If you refer to [DbContext](https://msdn.microsoft.com/en-us/library/system.data.entity.dbcontext(v=vs.113).aspx), it *does* implement [IObjectContextAdapter](https://msdn.microsoft.com/en-us/library/system.data.entity.infrastructure.iobjectcontextadapter(v=vs.113).aspx) which provides access to ObjectContext – Panagiotis Kanavos May 11 '17 at 15:26
  • 1
    what is myObject here? – Estevez Sep 07 '17 at 09:17
  • 7
    If you're struggling to get the ObjectStateManager, you may need to cast: var objectStateManager = ((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager; – FBryant87 Nov 20 '17 at 11:02
  • @FBryant87 that's what Sam asked three comments above. What you describe is about DbContext that was added 3-4 years later, not ObjectContext – Panagiotis Kanavos Nov 20 '17 at 12:30
  • Yep, just adding the code for those (like me) who needed it. – FBryant87 Nov 20 '17 at 12:55
  • Comments get deleted without warning. There are other specific questions about DbContext. Probably better too, now that ObjectContext is being removed – Panagiotis Kanavos Nov 20 '17 at 13:04
  • Is there any way i can implement this when inserting objects? – onhax Oct 11 '18 at 06:07
  • @onhax what do you mean? This code has nothing to do with the operation you intend to do, it returns the changes recorded in the context. Besides, when inserting an object there are no old values. The added object's properties will still appear in `CurrentValues` – Panagiotis Kanavos Oct 11 '18 at 06:25
  • @PanagiotisKanavos please see my question https://stackoverflow.com/questions/52752848/how-to-record-audit-changes-for-insert-add-c-sharp-ef4?noredirect=1#comment92429759_52752848 I am using your idea for logging changes on objects. But i can't implement this for insert/add. – onhax Oct 11 '18 at 06:29
  • @onhax the values will appear *after* you call `.Add`. The context knows nothing about the detached object you created before that – Panagiotis Kanavos Oct 11 '18 at 06:34
55

For EF5 upwards you can log your changes in the SaveChanges() method like this:

public override int SaveChanges()
{
    var changes = from e in this.ChangeTracker.Entries()
                  where e.State != System.Data.EntityState.Unchanged
                  select e;

    foreach (var change in changes)
    {
        if (change.State == System.Data.EntityState.Added)
        {
            // Log Added
        }
        else if (change.State == System.Data.EntityState.Modified)
        {
            // Log Modified
            var item = change.Cast<IEntity>().Entity;
            var originalValues = this.Entry(item).OriginalValues;
            var currentValues  = this.Entry(item).CurrentValues;
    
            foreach (string propertyName in originalValues.PropertyNames)
            {
                var original = originalValues[propertyName];
                var current = currentValues[propertyName];
        
                if (!Equals(original, current))
                {
                    // log propertyName: original --> current
                }
            }
        }
        else if (change.State ==  System.Data.EntityState.Deleted)
        {
            // log deleted
        }
    }
    
    // don't forget to save
    base.SaveChanges();
}
CarenRose
  • 1,266
  • 1
  • 12
  • 24
Jürgen Steinblock
  • 30,746
  • 24
  • 119
  • 189
10

I use this extension function that provides details on the entity being changed, the old and new values, the datatype, and the entity key.

This is tested with EF 6.1 using ObjectContext and uses log4net for output.

/// <summary>
/// dump changes in the context to the debug log
/// <para>Debug logging must be turned on using log4net</para>
/// </summary>
/// <param name="context">The context to dump the changes for</param>
public static void DumpChanges(this ObjectContext context)
{
    context.DetectChanges();
    
    // Output any added entries
    foreach (var added in context.ObjectStateManager.GetObjectStateEntries(EntityState.Added))
    {
        Log.DebugFormat("{0}:{1} {2} {3}", 
            added.State, 
            added.Entity.GetType().FullName, 
            added.Entity.ToString(), 
            string.Join(",", 
                added.CurrentValues.GetValue(1), 
                added.CurrentValues.GetValue(2))
            );
    }
    foreach (var modified in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
    {
        // Put original field values into dictionary
        var originalValues = new Dictionary<string,int>();
        for (var i = 0; i < modified.OriginalValues.FieldCount; ++i)
        {
            originalValues.Add(modified.OriginalValues.GetName(i), i);
        }
        
        // Output each of the changed properties.
        foreach (var entry in modified.GetModifiedProperties())
        {
            var originalIdx = originalValues[entry];
            Log.DebugFormat("{6} = {0}.{4} [{7}][{2}] [{1}] --> [{3}]  Rel:{5}", 
                modified.Entity.GetType(), 
                modified.OriginalValues.GetValue(originalIdx), 
                modified.OriginalValues.GetFieldType(originalIdx), 
                modified.CurrentValues.GetValue(originalIdx), 
                modified.OriginalValues.GetName(originalIdx), 
                modified.IsRelationship, 
                modified.State, 
                string.Join(",", 
                    modified.EntityKey.EntityKeyValues
                        .Select(v => string.Join(" = ", v.Key, v.Value))
                    )
                );
        }
    }
    
    // Output any deleted entries
    foreach (var deleted in context.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted))
    {
        Log.DebugFormat("{1} {0} {2}", 
            deleted.Entity.GetType().FullName, 
            deleted.State, 
            string.Join(",", 
                deleted.CurrentValues.GetValue(1), 
                deleted.CurrentValues.GetValue(2))
            );
    }
}
CarenRose
  • 1,266
  • 1
  • 12
  • 24
sweetfa
  • 5,457
  • 2
  • 48
  • 62
6

Use the IsModified field of each property, which is accessible by Context.Entry(Entity).Properties.

In this example, the modified entries are listed as a Tuple of the original and current values, indexed by name. Use any conversion that is required to build the audit log.

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;

//...

// gets somewhere in the scope
DbContext Context;
// Some entity that has been modified, but not saved and is being tracked by Context
object Entity;

//...

Dictionary<string, System.Tuple<object, object>> modified = 
    Context.Entry(Entity)
        .Properties.Where(p => p.IsModified)
        .ToDictionary(p => p.Metadata.Name, 
                      p => new System.Tuple<object,object>(p.OriginalValue, p.CurrentValue));
//...

Uses Entity Framework Core 3.1. Try it for EF 6.4, but it may not work.

CarenRose
  • 1,266
  • 1
  • 12
  • 24
marvin_x
  • 123
  • 1
  • 4