Let me start by saying that I ultimately used this SO answer from Mariusz Pawelski to put everything together.
I also came upon this based on a previous answer I had provided on SO for equality checking in XUnit. When I wrote that answer I was only concerned with comparing the values of properties of single instances of objects. Take the Person class:
1: public class Person
2: {
3: public string FirstName { get; set; }
4:
5: public string LastName { get; set; }
6: }
1: public class CustomEnumerableComparer<T> : IEqualityComparer<IEnumerable<T>>
2: {
3: public bool Equals(IEnumerable<T> expectedEnumerable, IEnumerable<T> actualEnumerable)
4: {
5: var expected = expectedEnumerable.ToList();
6: var actual = actualEnumerable.ToList();
7:
8: if (expected.Count != actual.Count)
9: {
10: throw new EqualException($"{expected.Count} items, $"{actual.Count} items");
11: }
12:
13: if (expected.Count == 0)
14: {
15: return true;
16: }
17:
18: var props = typeof(T).GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance);
19:
20: for (var i = 0; i < expected.Count; i++)
21: {
22: foreach (var prop in props)
23: {
24: var expectedValue = prop.GetValue(expected[i], null);
25: var actualValue = prop.GetValue(actual[i], null);
26:
27: if (expectedValue == null)
28: {
29: if (actualValue == null)
30: {
31: continue;
32: }
33:
34: throw new EqualException($"A value of null for property \"{prop.Name}\"",
35: $"A value of \"{actualValue}\" for property \"{prop.Name}\"");
36: }
37:
38: if (actualValue == null)
39: {
40: throw new EqualException($"A value of \"{expectedValue}\" for property \"{prop.Name}\"",
41: $"A value of null for property \"{prop.Name}\"");
42: }
43:
44: if (!expectedValue.Equals(actualValue))
45: {
46: throw new EqualException($"A value of \"{expectedValue}\" for property \"{prop.Name}\"",
47: $"A value of \"{actualValue}\" for property \"{prop.Name}\"");
48: }
49: }
50: }
51:
52: return true;
53: }
54:
55: public int GetHashCode(IEnumerable<T> parameterValue)
56: {
57: return Tuple.Create(parameterValue).GetHashCode();
58: }
59: }
This works perfectly! It works perfectly for a shallow equality check between two lists, that is. So if I was just comparing two lists of Person objects I'd be fine. However, if the Person class was changed to look like this:
1: public class Person
2: {
3: public List<Address> Addresses { get; set; }
4:
5: public string FirstName { get; set; }
6:
6: public string LastName { get; set; }
7: }
where the Address class looks like this:
1: public class Address
2: {
3: public string City { get; set; }
4:
5: public string Street1 { get; set; }
6:
6: public string Street2 { get; set; }
7: }
You can see that my comparer wouldn't work as I expect (again). That's because when it encountered the Addresses property, it'd do the same comparison it did before - it would check for properties on the List<> property, find none, and return true. I needed a way to recursively check equality on nested objects. That led me to the aforementioned SO answer as well as my own previous posts.
The first thing I did was move all of the code out of Equals into a private method called AreEqual with nearly the same method signature. Here's the signature only. I'll post all of the code at the very end.
1: private bool AreEqual<TNested>(IEnumerable<TNested> expectedEnumerable, IEnumerable<TNested> actualEnumerable)
Now that I have that separated out, I can recursively call it (that means call the AreEqual method from within the AreEqual method). The only thing I had left to do was determine when to do a recursive call and when to do a straight equality check. Using this other answer I found on SO (I spend a lot of time there some days) I was able to include some useful extension methods to determine whether my property was an enumerable.
At this point, I can do a deep equality check on a list and recursively do deep equality checks on properties of those objects in the list that are also lists themselves, but I still have one problem: How do I invoke the AreEqual argument from within itself given that the types of the nested lists aren't likely to be the same as their containing objects? Since AreEqual accepts a generic type parameter, I can invoke it like AreEqual
44: if(prop.IsNonStringEnumerable())
45: {
46: // get the type of the object in the enumerable
47: var objectType = prop.PropertyType.GetGenericArguments()[0];
48: // create a List
49: var genericListType = typeof(List<>).MakeGenericType(objectType);
50: // instantiate lists with the values in the enumerables
51: var expectedList = (IList)Activator.CreateInstance(genericListType, expectedValue);
52: var actualList = (IList)Activator.CreateInstance(genericListType, actualValue);
53: AreEqual((dynamic)expectedList, (dynamic)actualList);
54: }
We're not using the return value of AreEqual when we call it recursively because we've nested the throw statements in there so if we encounter a situation where two values (regardless of their depth) are not equal and they should be, processing will stop immediately.
OK, that was a long way to go to learn something you probably didn't even mean to read this post about (the post kinda turned into how to compare two values in XUnit). Thanks for sticking with me. Here's the full CustomEnumerableComparer class:
1: public class Person
2: {
3: public string FirstName { get; set; }
4:
5: public string LastName { get; set; }
6: }
1: public class CustomEnumerableComparer<T> : IEqualityComparer<IEnumerable<T>>
2: {
3: public bool Equals(IEnumerable<T> expectedEnumerable, IEnumerable<T> actualEnumerable)
4: {
5: return AreEqual(expectedEnumerable, actualEnumerable);
6: }
7:
8: public int GetHashCode(IEnumerable<T> parameterValue)
9: {
10: return Tuple.Create(parameterValue).GetHashCode();
11: }
12:
13: private bool AreEqual<TNested>(IEnumerable<TNested> expectedEnumerable, IEnumerable<TNested> actualEnumerable)
14: {
15: var expected = expectedEnumerable.ToList();
16: var actual = actualEnumerable.ToList();
17:
18: if (expected.Count != actual.Count)
19: {
20: throw new EqualException($"{expected.Count} items, $"{actual.Count} items");
21: }
22:
23: if (expected.Count == 0)
24: {
25: return true;
26: }
27:
28: var props = typeof(T).GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance);
29:
30: for (var i = 0; i < expected.Count; i++)
31: {
32: foreach (var prop in props)
33: {
34: var expectedValue = prop.GetValue(expected[i], null);
35: var actualValue = prop.GetValue(actual[i], null);
36:
37: if (expectedValue == null)
38: {
39: if (actualValue == null)
40: {
41: continue;
42: }
43:
44: throw new EqualException($"A value of null for property \"{prop.Name}\"",
45: $"A value of \"{actualValue}\" for property \"{prop.Name}\"");
46: }
47:
48: if (actualValue == null)
49: {
50: throw new EqualException($"A value of \"{expectedValue}\" for property \"{prop.Name}\"",
51: $"A value of null for property \"{prop.Name}\"");
52: }
53:
54: if (prop.IsNonStringEnumerable())
55: {
56: // get the type of the object in the enumerable
57: var objectType = prop.PropertyType.GetGenericArguments()[0];
58: // create a List
59: var genericListType = typeof(List<>).MakeGenericType(objectType);
60: // instantiate lists with the values in the enumerables
61: var expectedList = (IList)Activator.CreateInstance(genericListType, expectedValue);
62: var actualList = (IList)Activator.CreateInstance(genericListType, actualValue);
63: AreEqual((dynamic)expectedList, (dynamic)actualList);
64: }
65: else if (!expectedValue.Equals(actualValue))
66: {
67: throw new EqualException($"A value of \"{expectedValue}\" for property \"{prop.Name}\"",
68: $"A value of \"{actualValue}\" for property \"{prop.Name}\"");
69: }
70: }
71: }
72:
73: return true;
74: }
75: }
No comments:
Post a Comment