9

I want to get the names of all properties that changed for matching objects. I have these (simplified) classes:

public enum PersonType { Student, Professor, Employee }

class Person {
    public string Name { get; set; }
    public PersonType Type { get; set; }
}

class Student : Person {
     public string MatriculationNumber { get; set; }
}

class Subject {
     public string Name { get; set; }
     public int WeeklyHours { get; set; }
}

class Professor : Person {
    public List<Subject> Subjects { get; set; }
}

Now I want to get the objects where the Property values differ:

List<Person> oldPersonList = ...
List<Person> newPersonList = ...
List<Difference> = GetDifferences(oldPersonList, newPersonList);

public List<Difference> GetDifferences(List<Person> oldP, List<Person> newP) {
     //how to check the properties without casting and checking 
     //for each type and individual property??
     //can this be done with Reflection even in Lists??
}

In the end I would like to have a list of Differences like this:

class Difference {
    public List<string> ChangedProperties { get; set; }
    public Person NewPerson { get; set; }
    public Person OldPerson { get; set; }
}

The ChangedProperties should contain the name of the changed properties.

Kit
  • 20,354
  • 4
  • 60
  • 103
juergen d
  • 201,996
  • 37
  • 293
  • 362
  • Doing this for lists is a *real* pain (assuming you need to handle add/remove/re-order/etc); however, on a per-object basis, please see: http://stackoverflow.com/questions/3060382/comparing-2-objects-and-retrieve-a-list-of-fields-with-different-values - which does exactly this – Marc Gravell Jun 17 '13 at 20:12
  • @MarcGravell: I tried it and it returns the attributes that are lists as delta. Thanks anyway. – juergen d Jun 18 '13 at 22:50
  • Do you care about the properties that aren't in both objects, I.e. should matriculationNumber be considered a change when you compare a person to a student? – Patrick Hallisey Jun 20 '13 at 16:49
  • I match the persons with the `Name` Property and then compare them if a match is found. So only Objects of the same type can be compared. – juergen d Jun 20 '13 at 16:56
  • since Subjects is a list,what if 1 professor has 2 subject elements in list and other professor has for example just 1 or 3,that extra or lesser subject element would count as difference? – terrybozzio Jun 24 '13 at 16:38

5 Answers5

6

I've spent quite a while trying to write a faster reflection-based solution using typed delegates. But eventually I gave up and switched to Marc Gravell's Fast-Member library to achieve higher performance than with normal reflection.

Code:

internal class PropertyComparer
{    
    public static IEnumerable<Difference<T>> GetDifferences<T>(PropertyComparer pc,
                                                               IEnumerable<T> oldPersons,
                                                               IEnumerable<T> newPersons)
        where T : Person
    {
        Dictionary<string, T> newPersonMap = newPersons.ToDictionary(p => p.Name, p => p);
        foreach (T op in oldPersons)
        {
            // match items from the two lists by the 'Name' property
            if (newPersonMap.ContainsKey(op.Name))
            {
                T np = newPersonMap[op.Name];
                Difference<T> diff = pc.SearchDifferences(op, np);
                if (diff != null)
                {
                    yield return diff;
                }
            }
        }
    }

    private Difference<T> SearchDifferences<T>(T obj1, T obj2)
    {
        CacheObject(obj1);
        CacheObject(obj2);
        return SimpleSearch(obj1, obj2);
    }

    private Difference<T> SimpleSearch<T>(T obj1, T obj2)
    {
        Difference<T> diff = new Difference<T>
                                {
                                    ChangedProperties = new List<string>(),
                                    OldPerson = obj1,
                                    NewPerson = obj2
                                };
        ObjectAccessor obj1Getter = ObjectAccessor.Create(obj1);
        ObjectAccessor obj2Getter = ObjectAccessor.Create(obj2);
        var propertyList = _propertyCache[obj1.GetType()];
        // find the common properties if types differ
        if (obj1.GetType() != obj2.GetType())
        {
            propertyList = propertyList.Intersect(_propertyCache[obj2.GetType()]).ToList();
        }
        foreach (string propName in propertyList)
        {
            // fetch the property value via the ObjectAccessor
            if (!obj1Getter[propName].Equals(obj2Getter[propName]))
            {
                diff.ChangedProperties.Add(propName);
            }
        }
        return diff.ChangedProperties.Count > 0 ? diff : null;
    }

    // cache for the expensive reflections calls
    private Dictionary<Type, List<string>> _propertyCache = new Dictionary<Type, List<string>>();
    private void CacheObject<T>(T obj)
    {
        if (!_propertyCache.ContainsKey(obj.GetType()))
        {
            _propertyCache[obj.GetType()] = new List<string>();
            _propertyCache[obj.GetType()].AddRange(obj.GetType().GetProperties().Select(pi => pi.Name));
        }
    }
}

Usage:

PropertyComparer pc = new PropertyComparer();
var diffs = PropertyComparer.GetDifferences(pc, oldPersonList, newPersonList).ToList();

Performance:

My very biased measurements showed that this approach is about 4-6 times faster than the Json-Conversion and about 9 times faster than ordinary reflections. But in fairness, you could probably speed up the other solutions quite a bit.

Limitations:

At the moment my solution doesn't recurse over nested lists, for example it doesn't compare individual Subject items - it only detects that the subjects lists are different, but not what or where. However, it shouldn't be too hard to add this feature when you need it. The most difficult part would probably be to decide how to represent these differences in the Difference class.

Community
  • 1
  • 1
djf
  • 6,592
  • 6
  • 44
  • 62
3

We start with 2 simple methods:

public bool AreEqual(object leftValue, object rightValue)
{
    var left = JsonConvert.SerializeObject(leftValue);
    var right = JsonConvert.SerializeObject(rightValue);

    return left == right;
}

public Difference<T> GetDifference<T>(T newItem, T oldItem)
{
    var properties = typeof(T).GetProperties();

    var propertyValues = properties
        .Select(p => new { 
            p.Name, 
            LeftValue = p.GetValue(newItem), 
            RightValue = p.GetValue(oldItem) 
        });

    var differences = propertyValues
        .Where(p => !AreEqual(p.LeftValue, p.RightValue))
        .Select(p => p.Name)
        .ToList();

    return new Difference<T>
    {
        ChangedProperties = differences,
        NewItem = newItem,
        OldItem = oldItem
    };
}

AreEqual just compares the serialized versions of two objects using Json.Net, this keeps it from treating reference types and value types differently.

GetDifference checks the properties on the passed in objects and compares them individually.

To get a list of differences:

var oldPersonList = new List<Person> { 
    new Person { Name = "Bill" }, 
    new Person { Name = "Bob" }
};

var newPersonList = new List<Person> {
    new Person { Name = "Bill" },
    new Person { Name = "Bobby" }
};

var diffList = oldPersonList.Zip(newPersonList, GetDifference)
    .Where(d => d.ChangedProperties.Any())
    .ToList();
Patrick Hallisey
  • 888
  • 6
  • 11
2

Everyone always tries to get fancy and write these overly generic ways of extracting data. There is a cost to that.

Why not be old school simple.

Have a GetDifferences member function Person.

 virtual List<String> GetDifferences(Person otherPerson){
   var diffs = new List<string>();
   if(this.X != otherPerson.X) diffs.add("X");
   ....
 }

In inherited classes. Override and add their specific properties. AddRange the base function.

KISS - Keep it simple. It would take you 10 minutes of monkey work to write it, and you know it will be efficient and work.

  • 1
    This will work. But it won't take 10 minutes. My classes are far more complex that the example I gave. It will take at least a day, with hundrets lines of code and possible errors in that code. – juergen d Jun 24 '13 at 15:34
  • 2
    Make sure you weigh the perf on it then. If low number of comparisons, then the reflection route is fine. If you are running this over hundreds of thousands+ then I would be more wary. – CodeMonkeyForHire Jun 24 '13 at 15:39
0

I am doing it by using this:

    //This structure represents the comparison of one member of an object to the corresponding member of another object.
    public struct MemberComparison
    {
        public static PropertyInfo NullProperty = null; //used for ROOT properties - i dont know their name only that they are changed

        public readonly MemberInfo Member; //Which member this Comparison compares
        public readonly object Value1, Value2;//The values of each object's respective member
        public MemberComparison(PropertyInfo member, object value1, object value2)
        {
            Member = member;
            Value1 = value1;
            Value2 = value2;
        }

        public override string ToString()
        { 
            return Member.name+ ": " + Value1.ToString() + (Value1.Equals(Value2) ? " == " : " != ") + Value2.ToString();
        }
    }

    //This method can be used to get a list of MemberComparison values that represent the fields and/or properties that differ between the two objects.
    public static List<MemberComparison> ReflectiveCompare<T>(T x, T y)
    {
        List<MemberComparison> list = new List<MemberComparison>();//The list to be returned

        if (x.GetType().IsArray)
        {
            Array xArray = x as Array;
            Array yArray = y as Array;
            if (xArray.Length != yArray.Length)
                list.Add(new MemberComparison(MemberComparison.NullProperty, "array", "array"));
            else
            {
                for (int i = 0; i < xArray.Length; i++)
                {
                    var compare = ReflectiveCompare(xArray.GetValue(i), yArray.GetValue(i));
                    if (compare.Count > 0)
                        list.AddRange(compare);
                }
            }
        }
        else
        {
            foreach (PropertyInfo m in x.GetType().GetProperties())
                //Only look at fields and properties.
                //This could be changed to include methods, but you'd have to get values to pass to the methods you want to compare
                if (!m.PropertyType.IsArray && (m.PropertyType == typeof(String) || m.PropertyType == typeof(double) || m.PropertyType == typeof(int) || m.PropertyType == typeof(uint) || m.PropertyType == typeof(float)))
                {
                    var xValue = m.GetValue(x, null);
                    var yValue = m.GetValue(y, null);
                    if (!object.Equals(yValue, xValue))//Add a new comparison to the list if the value of the member defined on 'x' isn't equal to the value of the member defined on 'y'.
                        list.Add(new MemberComparison(m, yValue, xValue));
                }
                else if (m.PropertyType.IsArray)
                {
                    Array xArray = m.GetValue(x, null) as Array;
                    Array yArray = m.GetValue(y, null) as Array;
                    if (xArray.Length != yArray.Length)
                        list.Add(new MemberComparison(m, "array", "array"));
                    else
                    {
                        for (int i = 0; i < xArray.Length; i++)
                        {
                            var compare = ReflectiveCompare(xArray.GetValue(i), yArray.GetValue(i));
                            if (compare.Count > 0)
                                list.AddRange(compare);
                        }
                    }
                }
                else if (m.PropertyType.IsClass)
                {
                    var xValue = m.GetValue(x, null);
                    var yValue = m.GetValue(y, null);
                    if ((xValue == null || yValue == null) && !(yValue == null && xValue == null))
                        list.Add(new MemberComparison(m, xValue, yValue));
                    else if (!(xValue == null || yValue == null))
                    {
                        var compare = ReflectiveCompare(m.GetValue(x, null), m.GetValue(y, null));
                        if (compare.Count > 0)
                            list.AddRange(compare);
                    }


                }
        }
        return list;
    }
Martin Ch
  • 1,337
  • 4
  • 21
  • 42
0

Here you have a code which does what you want with Reflection.

    public List<Difference> GetDifferences(List<Person> oldP, List<Person> newP)
    {
        List<Difference> allDiffs = new List<Difference>();
        foreach (Person oldPerson in oldP)
        {
            foreach (Person newPerson in newP)
            {
                Difference curDiff = GetDifferencesTwoPersons(oldPerson, newPerson);
                allDiffs.Add(curDiff);
            }
        }

        return allDiffs;
    }

    private Difference GetDifferencesTwoPersons(Person OldPerson, Person NewPerson)
    {
        MemberInfo[] members = typeof(Person).GetMembers();

        Difference returnDiff = new Difference();
        returnDiff.NewPerson = NewPerson;
        returnDiff.OldPerson = OldPerson;
        returnDiff.ChangedProperties = new List<string>();
        foreach (MemberInfo member in members)
        {
            if (member.MemberType == MemberTypes.Property)
            {
                if (typeof(Person).GetProperty(member.Name).GetValue(NewPerson, null).ToString() != typeof(Person).GetProperty(member.Name).GetValue(OldPerson, null).ToString())
                {
                    returnDiff.ChangedProperties.Add(member.Name);
                }
            }
        }

        return returnDiff;
    }
varocarbas
  • 12,354
  • 4
  • 26
  • 37