Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Find differences between two entities of the same type

I'm working on an mvc3 web app. When the user updates something, I want to compare the old data to the new one the user is inputing and for each field that is different add those to a log to create an activity log.

Right now this is what my save action looks like:

[HttpPost]
public RedirectToRouteResult SaveSingleEdit(CompLang newcomplang)
{
    var oldCompLang = _db.CompLangs.First(x => x.Id == newcomplang.Id);

    _db.CompLangs.Attach(oldCompLang);
    newcomplang.LastUpdate = DateTime.Today;
    _db.CompLangs.ApplyCurrentValues(newcomplang);
    _db.SaveChanges();

    var comp = _db.CompLangs.First(x => x.Id == newcomplang.Id);

    return RedirectToAction("ViewSingleEdit", comp);
}

I found that I could use this to iterate through my property of oldCompLang:

var oldpropertyInfos = oldCompLang.GetType().GetProperties();

But this doesn't really help as it only shows me the properties (Id, Name, Status...) and not the values of these properties (1, Hello, Ready...).

I could just go the hard way:

if (oldCompLang.Status != newcomplang.Status)
{
    // Add to my activity log table something for this scenario
}

But I really don't want to be doing that for all the properties of the object.

I'm not sure what's the best way to iterate through both objects to find mismatches (for example the user changed the name, or the status...) and build a list from those differences that I can store in another table.

like image 866
LanFeusT Avatar asked Apr 27 '11 22:04

LanFeusT


3 Answers

It's not that bad, you can compare the properties "by hand" using reflection and write an extension methods for reuse - you can take this as a starting point:

public static class MyExtensions
{
    public static IEnumerable<string> EnumeratePropertyDifferences<T>(this T obj1, T obj2)
    {
        PropertyInfo[] properties = typeof(T).GetProperties();
        List<string> changes = new List<string>();

        foreach (PropertyInfo pi in properties)
        {
            object value1 = typeof(T).GetProperty(pi.Name).GetValue(obj1, null);
            object value2 = typeof(T).GetProperty(pi.Name).GetValue(obj2, null);

            if (value1 != value2 && (value1 == null || !value1.Equals(value2)))
            {
                changes.Add(string.Format("Property {0} changed from {1} to {2}", pi.Name, value1, value2));
            }
        }
        return changes;
    }
}
like image 177
BrokenGlass Avatar answered Nov 18 '22 00:11

BrokenGlass


If you are using EntityFramework you can get changes directly from the ObjectContext

Getting the state change entries:

  var modifiedEntries= this.ObjectStateManager.GetObjectStateEntries(EntityState.Modified);

Getting saved property names and original and changed values:

  for (int i = 0; i < stateChangeEntry.CurrentValues.FieldCount - 1; i++)
            {
                var fieldName = stateChangeEntry.OriginalValues.GetName(i);

                if (fieldName != changedPropertyName)
                    continue;

                var originalValue = stateChangeEntry.OriginalValues.GetValue(i).ToString();
                var changedValue = stateChangeEntry.CurrentValues.GetValue(i).ToString();
            }

This is better than @BrokenGlass's answer because this will go deep in the object graph for any changed states and will give you the changed properties of associated collections. It is also better because this reflects everything the ObjectContext will eventually save to the database. With the accepted solution you may get property changes that won't actually be persisted in the situation where you become disconnected via the object context.

With EF 4.0 you can also override the SaveChanges() method and wrap any auditing or activity in the same transaction as the eventual entity save meaning an audit trail won't exist without the entities being changed and vice versa. This guarantees an accurate log. If you can't guarantee an audit is accurate than its almost useless.

like image 9
John Farrell Avatar answered Nov 17 '22 23:11

John Farrell


Extending jfar's answer to provide some clarification and changes that I had to make to get it to work:

// Had to add this:
db.ChangeTracker.DetectChanges();
// Had to add using System.Data.Entity.Infrastructure; for this:
var modifiedEntries = ((IObjectContextAdapter)db).ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified);

foreach (var stateChangeEntry in modifiedEntries)
{
    for (int i = 0; i < stateChangeEntry.CurrentValues.FieldCount; i++)
    {
        var fieldName = stateChangeEntry.OriginalValues.GetName(i);
        var changedPropertyName = stateChangeEntry.CurrentValues.GetName(i);

        if (fieldName != changedPropertyName)
            continue;

        var originalValue = stateChangeEntry.OriginalValues.GetValue(i).ToString();
        var changedValue = stateChangeEntry.CurrentValues.GetValue(i).ToString();
        if (originalValue != changedValue)
        {
            // do stuff
            var foo = originalValue;
            var bar = changedValue;
        }

    }
}
like image 1
Hugh Seagraves Avatar answered Nov 17 '22 23:11

Hugh Seagraves