Search code examples
c#propertiescomparison

Compare Properties automatically


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.


Solution

  • 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.