[ACCEPTED]-Finding property differences between two C# objects-auditing

Accepted answer
Score: 29

IComparable is for ordering comparisons. Either use 63 IEquatable instead, or just use the static System.Object.Equals method. The 62 latter has the benefit of also working if 61 the object is not a primitive type but still 60 defines its own equality comparison by overriding 59 Equals.

object originalValue = property.GetValue(originalObject, null);
object newValue = property.GetValue(changedObject, null);
if (!object.Equals(originalValue, newValue))
{
    string originalText = (originalValue != null) ?
        originalValue.ToString() : "[NULL]";
    string newText = (newText != null) ?
        newValue.ToString() : "[NULL]";
    // etc.
}

This obviously isn't perfect, but if you're 58 only doing it with classes that you control, then 57 you can make sure it always works for your 56 particular needs.

There are other methods 55 to compare objects (such as checksums, serialization, etc.) but 54 this is probably the most reliable if the 53 classes don't consistently implement IPropertyChanged and 52 you want to actually know the differences.


Update 51 for new example code:

Address address1 = new Address();
address1.StateProvince = new StateProvince();

Address address2 = new Address();
address2.StateProvince = new StateProvince();

IList list = Utility.GenerateAuditLogMessages(address1, address2);

The reason that using 50 object.Equals in your audit method results in a "hit" is 49 because the instances are actually not equal!

Sure, the 48 StateProvince may be empty in both cases, but address1 and address2 still 47 have non-null values for the StateProvince property and 46 each instance is different. Therefore, address1 and 45 address2 have different properties.

Let's flip this 44 around, take this code as an example:

Address address1 = new Address("35 Elm St");
address1.StateProvince = new StateProvince("TX");

Address address2 = new Address("35 Elm St");
address2.StateProvince = new StateProvince("AZ");

Should 43 these be considered equal? Well, they will 42 be, using your method, because StateProvince does not 41 implement IComparable. That's the only reason why 40 your method reported that the two objects 39 were the same in the original case. Since 38 the StateProvince class does not implement IComparable, the tracker 37 just skips that property entirely. But 36 these two addresses are clearly not equal!

This 35 is why I originally suggested using object.Equals, because 34 then you can override it in the StateProvince method 33 to get better results:

public class StateProvince
{
    public string Code { get; set; }

    public override bool Equals(object obj)
    {
        if (obj == null)
            return false;

        StateProvince sp = obj as StateProvince;
        if (object.ReferenceEquals(sp, null))
            return false;

        return (sp.Code == Code);
    }

    public bool Equals(StateProvince sp)
    {
        if (object.ReferenceEquals(sp, null))
            return false;

        return (sp.Code == Code);
    }

    public override int GetHashCode()
    {
        return Code.GetHashCode();
    }

    public override string ToString()
    {
        return string.Format("Code: [{0}]", Code);
    }
}

Once you've done this, the 32 object.Equals code will work perfectly. Instead of naïvely 31 checking whether or not address1 and address2 literally 30 have the same StateProvince reference, it will actually 29 check for semantic equality.


The other way 28 around this is to extend the tracking code 27 to actually descend into sub-objects. In 26 other words, for each property, check the 25 Type.IsClass and optionally the Type.IsInterface property, and if true, then 24 recursively invoke the change-tracking method 23 on the property itself, prefixing any audit 22 results returned recursively with the property 21 name. So you'd end up with a change for 20 StateProvinceCode.

I use the above approach sometimes too, but 19 it's easier to just override Equals on the objects 18 for which you want to compare semantic equality 17 (i.e. audit) and provide an appropriate 16 ToString override that makes it clear what changed. It 15 doesn't scale for deep nesting but I think 14 it's unusual to want to audit that way.

The 13 last trick is to define your own interface, say 12 IAuditable<T>, which takes a second instance of the same 11 type as a parameter and actually returns 10 a list (or enumerable) of all of the differences. It's 9 similar to our overridden object.Equals method above 8 but gives back more information. This is 7 useful for when the object graph is really 6 complicated and you know you can't rely 5 on Reflection or Equals. You can combine this 4 with the above approach; really all you 3 have to do is substitute IComparable for your IAuditable and 2 invoke the Audit method if it implements that 1 interface.

Score: 21

This project on github checks nearly any type 2 of property and can be customized as you 1 need.

Score: 11

You might want to look at Microsoft's Testapi It has an object 3 comparison api that does deep comparisons. It 2 might be overkill for you but it could be 1 worth a look.

var comparer = new ObjectComparer(new PublicPropertyObjectGraphFactory());
IEnumerable<ObjectComparisonMismatch> mismatches;
bool result = comparer.Compare(left, right, out mismatches);

foreach (var mismatch in mismatches)
{
    Console.Out.WriteLine("\t'{0}' = '{1}' and '{2}'='{3}' do not match. '{4}'",
        mismatch.LeftObjectNode.Name, mismatch.LeftObjectNode.ObjectValue,
        mismatch.RightObjectNode.Name, mismatch.RightObjectNode.ObjectValue,
        mismatch.MismatchType);
}
Score: 3

Here a short LINQ version that extends object 2 and returns a list of properties that are 1 not equal:

usage: object.DetailedCompare(objectToCompare);

public static class ObjectExtensions
    {

        public static List<Variance> DetailedCompare<T>(this T val1, T val2)
        {
            var propertyInfo = val1.GetType().GetProperties();
            return propertyInfo.Select(f => new Variance
                {
                    Property = f.Name,
                    ValueA = f.GetValue(val1),
                    ValueB = f.GetValue(val2)
                })
                .Where(v => !v.ValueA.Equals(v.ValueB))
                .ToList();
        }

        public class Variance
        {
            public string Property { get; set; }
            public object ValueA { get; set; }
            public object ValueB { get; set; }
        }

    }
Score: 2

You never want to implement GetHashCode 9 on mutable properties (properties that could 8 be changed by someone) - i.e. non-private 7 setters.

Imagine this scenario:

  1. you put an instance of your object in a collection which uses GetHashCode() "under the covers" or directly (Hashtable).
  2. Then someone changes the value of the field/property that you've used in your GetHashCode() implementation.

Guess what...your 6 object is permanently lost in the collection 5 since the collection uses GetHashCode() to 4 find it! You've effectively changed the 3 hashcode value from what was originally 2 placed in the collection. Probably not 1 what you wanted.

Score: 1

Liviu Trifoi solution: Using CompareNETObjects library. GitHub - NuGet package - Tutorial.

0

Score: 0

I think this method is quite neat, it avoids 7 repetition or adding anything to classes. What 6 more are you looking for?

The only alternative 5 would be to generate a state dictionary 4 for the old and new objects, and write a 3 comparison for them. The code for generating 2 the state dictionary could reuse any serialisation 1 you have for storing this data in the database.

Score: 0

The my way of Expression tree compile version. It 1 should faster than PropertyInfo.GetValue.

static class ObjDiffCollector<T>
{
    private delegate DiffEntry DiffDelegate(T x, T y);

    private static readonly IReadOnlyDictionary<string, DiffDelegate> DicDiffDels;

    private static PropertyInfo PropertyOf<TClass, TProperty>(Expression<Func<TClass, TProperty>> selector)
        => (PropertyInfo)((MemberExpression)selector.Body).Member;

    static ObjDiffCollector()
    {
        var expParamX = Expression.Parameter(typeof(T), "x");
        var expParamY = Expression.Parameter(typeof(T), "y");

        var propDrName = PropertyOf((DiffEntry x) => x.Prop);
        var propDrValX = PropertyOf((DiffEntry x) => x.ValX);
        var propDrValY = PropertyOf((DiffEntry x) => x.ValY);

        var dic = new Dictionary<string, DiffDelegate>();

        var props = typeof(T).GetProperties();
        foreach (var info in props)
        {
            var expValX = Expression.MakeMemberAccess(expParamX, info);
            var expValY = Expression.MakeMemberAccess(expParamY, info);

            var expEq = Expression.Equal(expValX, expValY);

            var expNewEntry = Expression.New(typeof(DiffEntry));
            var expMemberInitEntry = Expression.MemberInit(expNewEntry,
                Expression.Bind(propDrName, Expression.Constant(info.Name)),
                Expression.Bind(propDrValX, Expression.Convert(expValX, typeof(object))),
                Expression.Bind(propDrValY, Expression.Convert(expValY, typeof(object)))
            );

            var expReturn = Expression.Condition(expEq
                , Expression.Convert(Expression.Constant(null), typeof(DiffEntry))
                , expMemberInitEntry);

            var expLambda = Expression.Lambda<DiffDelegate>(expReturn, expParamX, expParamY);

            var compiled = expLambda.Compile();

            dic[info.Name] = compiled;
        }

        DicDiffDels = dic;
    }

    public static DiffEntry[] Diff(T x, T y)
    {
        var list = new List<DiffEntry>(DicDiffDels.Count);
        foreach (var pair in DicDiffDels)
        {
            var r = pair.Value(x, y);
            if (r != null) list.Add(r);
        }
        return list.ToArray();
    }
}

class DiffEntry
{
    public string Prop { get; set; }
    public object ValX { get; set; }
    public object ValY { get; set; }
}

More Related questions