nitish
nitish

Reputation: 81

Compare list/IEnumerable type properties in Generic Method

I am trying to compare the object which contains List<> type properties. I am able to compare the simple properties but got stuck with complex one.

 foreach (PropertyInfo pi in properties)
     {
        object oldValue = pi.GetValue(oldObject), newValue = pi.GetValue(newObject);
        if (pi.PropertyType.IsGenericType && typeof(IEnumerable).IsAssignableFrom(pi.PropertyType))
        {
           Type type = oldValue.GetType().GetGenericArguments()[0];

           /* Need something like below commented line.*/
           // var added = newValue.Except(oldValue)
           // var removed = oldValue.Except(newValue);
        }}

In if block, i need to find the added and removed object in List type properties. In object we have Key attributed property to find added and removed object.

Upvotes: 0

Views: 760

Answers (2)

felix-b
felix-b

Reputation: 8498

Well, here is the complete solution according to my understanding of the question.

here is the key attribute that designates key property of an item:

[AttributeUsage(AttributeTargets.Property)]
public class KeyAttribute : Attribute
{
}

For test, let's say we have a class named SomeClass, which contains a List<> property, and an item class named SomeItem, which contains one key property, and additional properties ignored by comparison:

public class SomeClass
{
    public List<SomeItem> Items { get; set; }
}

public class SomeItem
{
    [Key]
    public int TheKey { get; set; }
    public string SomeValue { get; set; }
}

Here is the function that performs the comparison:

public void CompareNewWithOld(object oldObject, object newObject, List<object> added, List<object> removed)
{
    var properties = typeof (SomeClass).GetProperties();

    foreach (PropertyInfo pi in properties)
    {
        object oldValue = pi.GetValue(oldObject), newValue = pi.GetValue(newObject);
        if (pi.PropertyType.IsGenericType && typeof(IEnumerable).IsAssignableFrom(pi.PropertyType))
        {
            var itemType = pi.PropertyType.GetGenericArguments()[0];
            var itemKeyProperty = itemType
                .GetProperties()
                .FirstOrDefault(ipi => ipi.GetCustomAttribute<KeyAttribute>() != null);

            if (itemKeyProperty == null)
            {
                continue; // no Key property -- cannot compare
            }

            var comparer = new ItemByKeyEqualityComparer(itemKeyProperty);

            HashSet<object> oldSet = new HashSet<object>(((IEnumerable)oldValue).Cast<object>(), comparer);
            HashSet<object> newSet = new HashSet<object>(((IEnumerable)newValue).Cast<object>(), comparer);

            HashSet<object> removedSet = new HashSet<object>(oldSet, comparer);
            removedSet.ExceptWith(newSet);

            HashSet<object> addedSet = new HashSet<object>(newSet, comparer);
            addedSet.ExceptWith(oldSet);

            added.AddRange(addedSet);
            removed.AddRange(removedSet);
        }
    }
}

In order to conveniently compare item objects by their key property with HashSet<T>, we also need to implement an equality comparer class, as follows:

public class ItemByKeyEqualityComparer : IEqualityComparer<object>
{
    private readonly PropertyInfo _keyProperty;
    public ItemByKeyEqualityComparer(PropertyInfo keyProperty)
    {
        _keyProperty = keyProperty;
    }
    public bool Equals(object x, object y)
    {
        var kx = _keyProperty.GetValue(x);
        var ky = _keyProperty.GetValue(y);
        if (kx == null)
        {
            return (ky == null);
        }
        return kx.Equals(ky);
    }
    public int GetHashCode(object obj)
    {
        var key = _keyProperty.GetValue(obj);
        return (key == null ? 0 : key.GetHashCode());
    }
}

and here is a test that passes:

[Test]
public void TestCompareNewWithOld()
{
    var oldObject = new SomeClass() {
        Items = new List<SomeItem>() {
            new SomeItem() { TheKey = 1, SomeValue = "A"},
            new SomeItem() { TheKey = 2, SomeValue = "B"},
            new SomeItem() { TheKey = 3, SomeValue = "C"},
            new SomeItem() { TheKey = 4, SomeValue = "D"},
        }
    };
    var newObject = new SomeClass() {
        Items = new List<SomeItem>() {
            new SomeItem() { TheKey = 3, SomeValue = "W"},
            new SomeItem() { TheKey = 4, SomeValue = "V"},
            new SomeItem() { TheKey = 5, SomeValue = "U"},
            new SomeItem() { TheKey = 6, SomeValue = "T"},
        }
    };

    var added = new List<object>();
    var removed = new List<object>();

    CompareNewWithOld(oldObject, newObject, added, removed);

    Assert.That(removed, Is.EquivalentTo(new[] {
        oldObject.Items[0],  //A
        oldObject.Items[1]   //B
    }));
    Assert.That(added, Is.EquivalentTo(new[] {
        newObject.Items[2],  //U
        newObject.Items[3]   //T
    }));
}

Upvotes: 3

Andrew Sklyarevsky
Andrew Sklyarevsky

Reputation: 2135

Cast oldValue and newValue to IEnumerable<Object> and then compare them as needed:

if (IsGenericEnumerable(pi)) {
    IEnumerable<Object> newEnumerable = (IEnumerable<Object>) newValue;
    IEnumerable<Object> oldEnumerable = (IEnumerable<Object>) oldValue;

    // operate with newEnumerable and oldEnumerable as needed by the logic
    // ...
}

Upvotes: 0

Related Questions