Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 241
juergen d Avatar asked Jun 17 '13 20:06

juergen d


3 Answers

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.

like image 67
djf Avatar answered Nov 11 '22 04:11

djf


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();
like image 41
Patrick Hallisey Avatar answered Nov 11 '22 05:11

Patrick Hallisey


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.

like image 2
CodeMonkeyForHire Avatar answered Nov 11 '22 04:11

CodeMonkeyForHire