1

I have a generic object comparison method which I use to compare two models with the same structure.

public static List<Variance> DetailedCompare<T>(this T val1, T val2)
  {
    var variances = new List<Variance>();

    var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
    foreach (var property in properties.Where(t => t.IsMarkedWith<IncludeInComparisonAttribute>()))
      {
        var v = new Variance
          {
            PropertyName = property.Name,
            ValA = property.GetValue(val1, null),
            ValB = property.GetValue(val2, null)
          };

          if (v.ValA == null && v.ValB == null) { continue; }

          if (v.ValA != null && !v.ValA.Equals(v.ValB))
            {
              variances.Add(v);
            }
          }
        return variances;
    }

The problem I have is that sometimes an object is passed to it that may contain a list of other objects within it. Because it only compares at the top level it just returns that the object array was changed. Ideally I would like it to go through the nested array and look at the changed values as well.

Ideally I think it should probably make a recursive call when it finds an object array. Any ideas how I might go about this?

Edit - with working examples

Here are some .net fiddle examples of how this is meant to work.

This is the first code example that doesn't search down through the nested objects and just reports that the collection has changed (as per the code above):

https://dotnetfiddle.net/Cng7GI

returns:

Property: NumberOfDesks has changed from '5' to '4' Property: Students has changed from 'System.Collections.Generic.List1[Student]' to 'System.Collections.Generic.List1[Student]'

Now if I try and call the DetailedCompare if I find a nested array using:

        if (v.ValA is ICollection)
            {
                Console.WriteLine("I found a nested list");
                variances.AddRange(v.ValA.DetailedCompare(v.ValB));
            } 
        else if(v.ValA != null && !v.ValA.Equals(v.ValB)){
            variances.Add(v);
        }

it doesn't look like the recursive call works

https://dotnetfiddle.net/Ns1tx5

as I just get:

I found a nested list Property: NumberOfDesks has changed from '5' to '4'

If I add:

var list = v.ValA.DetailedCompare<T>(v.ValB);

inside the Collection check, I get an error that:

object does not contain a definition for 'DetailedCompare' ... Cannot convert instance argument type 'object' to T

really what I want from it is just a single array of all the property names and their value changes.

Property: NumberOfDesks has changed from '5' to '4'

Property: Id has changed from '1' to '4'

Property: FirstName has changed from 'Cheshire' to 'Door'

etc

6
  • The challenge would be storing those variances for the nested objects. Because they'll be done within.. or do you perhaps want to have one variables variable containing variances for all levels? Commented Aug 13, 2020 at 23:20
  • I suppose you don't want to do that, How about you do it but set top level variances as a dictionary with other bottom level variables stored with their keys... e.g dictionary["level1"] or dictionary["level2"] etc... Would you appreciate that? Commented Aug 13, 2020 at 23:22
  • You have to be careful with the equality comparisons for reference types (except for string), where you could get undesired results. Perhaps you are overriding equality for all your custom classes, otherwise two different instances with the same values will not be equal. Commented Aug 13, 2020 at 23:37
  • Thanks. I'm happy to have all of the differences in the single variances array Commented Aug 14, 2020 at 8:33
  • 1
    You already answered your question, you need recursion. You can have a check like this if (ValA is IEnumerable eA && ValB is IEnumerable eB) and do some custom logic where you iterate through and call DetailCompare. Then you just need to make an overload for DetailCompare that takes a Type instead of <T> Commented Aug 15, 2020 at 10:55

1 Answer 1

2
+50

Calling the method recursively is the issue here.

If we call a method DetailedCompare recursively passing as parameters two objects all its fine - as we can get their properties and compare them.

However when we call DetailedCompare recursively passing a two list of objects - we can not just get the properties of those lists - but we need to traverse and get the properties of those list and compare their value.

IMHO it would be better to separate the logic using a helper method - so when we find a nested list - we can tackle the logic as I have described above.

This is the Extension class I have written

  public static class Extension
    {
        public static List<Variance> Variances { get; set; }

        static Extension()
        {
            Variances = new List<Variance>();
        }

        public static List<Variance> DetailedCompare<T>(this T val1, T val2)
        {
            var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (var property in properties)
            {
                var v = new Variance
                {
                    PropertyName = property.Name,
                    ValA = property.GetValue(val1, null),
                    ValB = property.GetValue(val2, null)
                };

                if (v.ValA == null && v.ValB == null)
                {
                    continue;
                }

                if (v.ValA is ICollection)
                {
                    Console.WriteLine("I found a nested list");
                    DetailedCompareList(v.ValA,v.ValB);
                }
                else if (v.ValA != null && !v.ValA.Equals(v.ValB))
                {
                    Variances.Add(v);
                }
            }

            return Variances;
        }

        private static void DetailedCompareList<T>(T val1, T val2)
        {
            if (val1 is ICollection collection1 && val2 is ICollection collection2)
            {
                var coll1 = collection1.Cast<object>().ToList();
                var coll2 = collection2.Cast<object>().ToList();

                for (int j = 0; j < coll1.Count; j++)
                {
                    Type type = coll1[j].GetType();
                    PropertyInfo[] propertiesOfCollection1 = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
                    PropertyInfo[] propertiesOfCollection2 = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

                    for (int i = 0; i < propertiesOfCollection1.Length; i++)
                    {
                        var variance = new Variance
                        {
                            PropertyName = propertiesOfCollection1[i].Name,
                            ValA = propertiesOfCollection1[i].GetValue(coll1[j]),
                            ValB = propertiesOfCollection2[i].GetValue(coll2[j])
                        };

                        if (!variance.ValA.Equals(variance.ValB))
                        {
                            Variances.Add(variance);
                        }
                    }
                }
            }
        }
    }

With the following result: enter image description here

Limitations This approach is bounded by the definition of your objects - hence it can only work with 1 level of depth.

Sign up to request clarification or add additional context in comments.

2 Comments

Great, thanks. Looks like it will do the job. In my code though v.Valb comes through as a Hashset. Any ideas about how I can deal with that? The casting DetailedCompareList in the if doesn't work on it
I changed the code to check if it is IEnumerable and it's worked. Thanks for your help. if (val1 is IEnumerable collection1 && val2 is IEnumerable collection2)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.