16

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.

LanFeusT
  • 2,392
  • 5
  • 38
  • 53

5 Answers5

26

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;
    }
}
BrokenGlass
  • 158,293
  • 28
  • 286
  • 335
  • My addition, if the property is a datetime you need to convert the proerties: `if (selfValue is DateTime && toValue is DateTime) { DateTime from = (DateTime)selfValue; DateTime to = (DateTime)toValue; if (!DateTime.Equals(from, to)) { }` – JoeJoe87577 May 04 '15 at 13:16
9

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.

John Farrell
  • 24,673
  • 10
  • 77
  • 110
  • I like the look of this answer but my code won't recognise the ObjectStateManager as a method of this. Has me stumped, what do you think? – GP24 Feb 12 '13 at 06:30
  • 1
    I know is old but is an excelent solution, two details only for those didn't see it: 1- between the 2 block of code you need a foreach( var stateChangeEntry in modifiedEntries) and 2- remove the "-1" in the for loop. – Santiago Sep 01 '13 at 20:06
  • Correct me if I'm wrong but, for this to work you need to keep your context open, meaning you have to use one context for your entire viewmodel (That is if you want to be able to detect changes in multiple places). Just attaching the entity doesn't work either. In my case @BrokenGlass solution works better since I wrap all my context call's in usings. – Bracher Jul 29 '15 at 08:28
1

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;
        }

    }
}
Hugh Seagraves
  • 594
  • 1
  • 8
  • 14
0

use this custom code to compare 2 objects. If they fail log it. Design point of view Create this in Utility class.

use it as object1.IsEqualTo(object2)

Community
  • 1
  • 1
Praneeth
  • 2,527
  • 5
  • 30
  • 47
0

Implement IEquatable<T> on your entity. Another way is implementing custom IComparer<T> where you can for example expose event which will be fired if property is different.

Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • This is a lot more complicated that I expected. I would have imagined this to be a pretty common issue that would have a simple solution to it ^^ I'm not even sure how to implement that IEquatable on my entity :/ – LanFeusT Apr 27 '11 at 23:31