Using Objects Comparer To Compare Complex Objects In C#

Introduction

It is quite a common situation when complex objects need to be compared. Sometimes, objects can contain nested elements, or some members should be excluded from the comparison (like auto-generated identifiers, create/update date etc.), or some members can have custom comparison rules (same data in different formats, like phone numbers). This small framework was developed to solve such kinds of problems.

Briefly, Objects Comparer is an object-to-object comparer that allows us to compare objects recursively member by member and to define custom comparison rules for certain properties, fields or types.

Objects Comparer can be considered as a ready-to-use framework or as a starting point for similar solutions.

Now, Objects Comparer supports enumerables (arrays, collections, lists), multidimensional arrays, enumerations, flags and dynamic objects (ExpandoObject, DynamicObject and compiler generated dynamic objects).

Installation

Objects Comparer can be installed as NuGet package.

Install-Package ObjectsComparer

Source code can be found on GitHub.

What's new in version 1.1
  • Dynamic objects support (ExpandoObject, DynamicObject, and compiler-generated dynamic objects)
  • Overriding comparison rule by member name
  • Overriding comparison rule by type and by name with filter
  • The DifferenceType property was added to the Difference class.

If you use version 1.0.x, you do not need to make any changes to start using version 1.1.

Basic Examples

To show how to use Objects Comparer, let's create 2 classes.

  1. public class ClassA  
  2. {  
  3.     public string StringProperty { getset; }  
  4.   
  5.     public int IntProperty { getset; }  
  6.   
  7.     public SubClassA SubClass { getset; }  
  8. }  
  9.   
  10. public class SubClassA  
  11. {  
  12.     public bool BoolProperty { getset; }  
  13. }  
 There are some examples below how Objects Comparer can be used to compare instances of these classes.
  1. //Initialize objects and comparer  
  2. var a1 = new ClassA { StringProperty = "String", IntProperty = 1 };  
  3. var a2 = new ClassA { StringProperty = "String", IntProperty = 1 };  
  4. var comparer = new Comparer<ClassA>();  
  5.   
  6. //Compare objects  
  7. IEnumerable<Difference> differences;  
  8. var isEqual = comparer.Compare(a1, a2, out differences);  
  9.   
  10. //Print results  
  11. Debug.WriteLine(isEqual ? "Objects are equal" : string.Join(Environment.NewLine, differenses));   

Objects are equal

In examples below, Compare objects and Print results blocks will be skipped for brevity except some cases.

  1. var a1 = new ClassA { StringProperty = "String", IntProperty = 1 };  
  2. var a2 = new ClassA { StringProperty = "String", IntProperty = 2 };  
  3. var comparer = new Comparer<ClassA>();   

Difference: DifferenceType=ValueMismatch, MemberPath='IntProperty', Value1='1', Value2='2'.

  1. var a1 = new ClassA { SubClass = new SubClassA { BoolProperty = true } };  
  2. var a2 = new ClassA { SubClass = new SubClassA { BoolProperty = false } };  
  3. var comparer = new Comparer<ClassA>();   

Difference: DifferenceType=ValueMismatch, MemberPath='SubClass.BoolProperty', Value1='True', Value2='False'.

Enumerables (arrays, collections, lists, etc.)

In this case, the enumerables can have different number of elements or some elements can have different values. Enumerables can be generic or non-generic. In case of non-generic enumerables, elements with the same index will be compared if types of these elements are equal, otherwise the difference with DifferenceType=TypeMismatch will be added to the list of differences.

  1. var a1 = new[] { 1, 2, 3 };  
  2. var a2 = new[] { 1, 2, 3 };  
  3. var comparer = new Comparer<int[]>();   

Objects are equal

  1. var a1 = new[] { 1, 2 };  
  2. var a2 = new[] { 1, 2, 3 };  
  3. var comparer = new Comparer<int[]>();   
Difference: DifferenceType=ValueMismatch, MemberPath='Length', Value1='2', Value2='3'.
  1. var a1 = new[] { 1, 2, 3 };  
  2. var a2 = new[] { 1, 4, 3 };  
  3. var comparer = new Comparer<int[]>(); 
Difference: DifferenceType=ValueMismatch, MemberPath='[1]', Value1='2', Value2='4'.
  1. var a1 = new ArrayList { "Str1""Str2" };  
  2. var a2 = new ArrayList { "Str1", 5 };  
  3. var comparer = new Comparer<ArrayList>(); 
Difference: DifferenceType=TypeMismatch, MemberPath='[1]', Value1='Str2', Value2='5'.

Multidimensional arrays
  1. var a1 = new[] { new[] { 1, 2 } };  
  2. var a2 = new[] { new[] { 1, 3 } };  
  3. var comparer = new Comparer<int[][]>();   
Difference: DifferenceType=ValueMismatch, MemberPath='[0][1]', Value1='2', Value2='3'.
  1. var a1 = new[] { new[] { 1, 2 } };  
  2. var a2 = new[] { new[] { 2, 2 }, new[] { 3, 5 } };  
  3. var comparer = new Comparer<int[][]>();   
Difference: DifferenceType=ValueMismatch, MemberPath='Length', Value1='1', Value2='2'.
  1. var a1 = new[] { new[] { 1, 2 }, new[] { 3, 5 } };  
  2. var a2 = new[] { new[] { 1, 2 }, new[] { 3, 5, 6 } };  
  3. var comparer = new Comparer<int[][]>(); 
Difference: DifferenceType=ValueMismatch, MemberPath='[1].Length', Value1='2', Value2='3'.
  1. var a1 = new[,] { { 1, 2 }, { 1, 3 } };  
  2. var a2 = new[,] { { 1, 3, 4 }, { 1, 3, 8 } };  
  3. var comparer = new Comparer<int[,]>();   
Difference: DifferenceType=ValueMismatch, MemberPath='Dimension1', Value1='2', Value2='3'.
  1. var a1 = new[,] { { 1, 2 } };  
  2. var a2 = new[,] { { 1, 3 } };  
  3. var comparer = new Comparer<int[,]>();   
Difference: DifferenceType=ValueMismatch, MemberPath='[0,1]', Value1='2', Value2='3'.

Dynamic objects

C# supports several types of objects, whose members can be dynamically added and removed at runtime.

ExpandoObject

If you are not familiar with how to use ExpandoObject, you can read this or search for another example.

  1. dynamic a1 = new ExpandoObject();  
  2. a1.Field1 = "A";  
  3. a1.Field2 = 5;  
  4. a1.Field4 = 4;  
  5. dynamic a2 = new ExpandoObject();  
  6. a2.Field1 = "B";  
  7. a2.Field3 = false;  
  8. a2.Field4 = "C";  
  9. var comparer = new Comparer();   

Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=MissedMemberInSecondObject, MemberPath='Field2', Value1='5', Value2=''.

Difference: DifferenceType=TypeMismatch, MemberPath='Field4', Value1='4', Value2='C'.

Difference: DifferenceType=MissedMemberInFirstObject, MemberPath='Field3', Value1='', Value2='False'.

  1. dynamic a1 = new ExpandoObject();  
  2. a1.Field1 = "A";  
  3. a1.Field2 = 5;  
  4. dynamic a2 = new ExpandoObject();  
  5. a2.Field1 = "B";  
  6. a2.Field3 = false;  
  7. var comparer = new Comparer();   
Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=MissedMemberInSecondObject, MemberPath='Field2', Value1='5', Value2=''.

Difference: DifferenceType=MissedMemberInFirstObject, MemberPath='Field3', Value1='', Value2='False'.

Behavior if member doesn't exist could be changed by providing custom ComparisonSettings (see Comparison Settings below).

  1. dynamic a1 = new ExpandoObject();  
  2. a1.Field1 = "A";  
  3. a1.Field2 = 0;  
  4. dynamic a2 = new ExpandoObject();  
  5. a2.Field1 = "B";  
  6. a2.Field3 = false;  
  7. a2.Field4 = "S";  
  8. var comparer = new Comparer(new ComparisonSettings { UseDefaultIfMemberNotExist = true });   
Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=ValueMismatch, MemberPath='Field4', Value1='', Value2='S'.

DynamicObject

DynamicObject is an abstract class and cannot be instantiated directly. Let’s assume that we have such implementation of the DynamicObject class. It is necessary to have a correct implementation of the method GetDynamicMemberNames, otherwise, the Objects Comparer wouldn't work in a right way.

If you are not familiar with how to use DynamicObject, you can read this or search for another example.

  1. private class DynamicDictionary : DynamicObject  
  2. {  
  3.     public int IntProperty { getset; }  
  4.   
  5.     private readonly Dictionary<stringobject> _dictionary = new Dictionary<stringobject>();  
  6.   
  7.     public override bool TryGetMember(GetMemberBinder binder, out object result)  
  8.     {  
  9.         var name = binder.Name;  
  10.   
  11.         return _dictionary.TryGetValue(name, out result);  
  12.     }  
  13.   
  14.     public override bool TrySetMember(SetMemberBinder binder, object value)  
  15.     {  
  16.         _dictionary[binder.Name] = value;  
  17.   
  18.         return true;  
  19.     }  
  20.   
  21.     public override IEnumerable<string> GetDynamicMemberNames()  
  22.     {  
  23.         return _dictionary.Keys;  
  24.     }  
  25. }  
  1. dynamic a1 = new DynamicDictionary();  
  2. a1.Field1 = "A";  
  3. a1.Field3 = true;  
  4. dynamic a2 = new DynamicDictionary();  
  5. a2.Field1 = "B";  
  6. a2.Field2 = 8;  
  7. a2.Field3 = 1;  
  8. var comparer = new Comparer();  

Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=TypeMismatch, MemberPath='Field3', Value1='True', Value2='1'.

Difference: DifferenceType=MissedMemberInFirstObject, MemberPath='Field2', Value1='', Value2='8'.

Compiler-generated objects

This type of dynamic object is most popular and most easy to create.

  1. dynamic a1 = new  
  2. {  
  3.     Field1 = "A",  
  4.     Field2 = 5,  
  5.     Field3 = true  
  6. };  
  7. dynamic a2 = new  
  8. {  
  9.     Field1 = "B",  
  10.     Field2 = 8  
  11. };  
  12. var comparer = new Comparer();  
  13.   
  14. IEnumerable<Difference> differences;  
  15. var isEqual = comparer.Compare((object)a1, (object)a2, out differences);   

Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=TypeMismatch, MemberPath='Field2', Value1='5', Value2='8'.

Difference: DifferenceType=MissedMemberInSecondObject, MemberPath='Field3', Value1='True', Value2=''.

This example requires some additional explanations. Types of the objects a1 and a2 were generated by the compiler and are considered as the same type if and only if objects a1 and a2 have the same set of members (same name and same type). If casting to (object) is skipped in case of a different set of members RuntimeBinderException will be thrown.

Overriding Comparison Rules

Sometimes some of the members require custom comparison logic. To override comparison rule we need to create a custom value comparer or provide function how to compare objects and how to convert these objects to the string (optional) and filter function(optional). Value Comparer should be inherited from AbstractValueComparer or should implement IValueComparer.

  1. public class MyValueComparer: AbstractValueComparer<string>  
  2. {  
  3.     public override bool Compare(string obj1, string obj2, ComparisonSettings settings)  
  4.     {  
  5.         return obj1 == obj2; //Implement comparison logic here  
  6.     }  
  7. }   

Override the comparison rule for objects of particular type.

  1. //Use MyComparer to compare all members of type string   
  2. comparer.AddComparerOverride<string>(new MyValueComparer());  
  3. comparer.AddComparerOverride(typeof(string), new MyValueComparer());  
  4. //Use MyComparer to compare all members of type string except members which name starts with "Xyz"  
  5. comparer.AddComparerOverride(typeof(string), new MyValueComparer(), member => !member.Name.StartsWith("Xyz"));  
  6. comparer.AddComparerOverride<string>(new MyValueComparer(), member => !member.Name.StartsWith("Xyz"));   

Override the comparison rule for particular member (Field or Property). If toStringFunction parameter is not provided objects will be converted to string using ToString() method.

  1. //Use MyValueComparer to compare StringProperty of ClassA  
  2. comparer.AddComparerOverride(() => new ClassA().StringProperty, new MyValueComparer());  
  3. comparer.AddComparerOverride(  
  4.     typeof(ClassA).GetTypeInfo().GetMember("StringProperty").First(),  
  5.     new MyValueComparer());  
  6. //Compare StringProperty of ClassA by length. If length equal consider that values are equal  
  7. comparer.AddComparerOverride(  
  8.     () => new ClassA().StringProperty,  
  9.     (s1, s2, parentSettings) => s1?.Length == s2?.Length,  
  10.     s => s?.ToString());  
  11. comparer.AddComparerOverride(  
  12.     () => new ClassA().StringProperty,  
  13.     (s1, s2, parentSettings) => s1?.Length == s2?.Length);   

Override the comparison rule for particular member(s) (Field or Property) by name.

  1. //Use MyValueComparer to compare all members with name equal to "StringProperty"  
  2. comparer.AddComparerOverride("StringProperty"new MyValueComparer());   

Overrides by type have highest priority, then overrides by member and overrides by member name have lowest priority. If more than one of value comparers of the same type (by type/by name/by member name) could be applied to the same member, exception AmbiguousComparerOverrideResolutionException will be thrown during comparison.

Example

  1. var a1 = new ClassA();  
  2. var a2 = new ClassA();  
  3. comparer.AddComparerOverride<string>(valueComparer1, member => member.Name.StartsWith("String"));  
  4. comparer.AddComparerOverride<string>(valueComparer2, member => member.Name.EndsWith("Property"));  
  5.   
  6. var result = comparer.Compare(a1, a2);//Exception here   
Comparison Settings

Comparer constructor has an optional settings parameter to configure some aspects of comparison.

RecursiveComparison

True by default. If true, all members which are not primitive types, do not have custom comparison rule and do not implement ICompareble will be compared using the same rules as root objects.

EmptyAndNullEnumerablesEqual

False by default. If true, empty enumerable (arrays, collections, lists etc.) and null values will be considered as equal values.

UseDefaultIfMemberNotExist

If true and the member does not exist, Objects Comparer will consider that this member is equal to the default value of opposite member type. Applicable for dynamic types comparison only. False by default.

Comparison Settings class allows storing custom values that can be used in custom comparers.

  1. SetCustomSetting<T>(T value, string key = null)  
  2. GetCustomSetting<T>(string key = null)   
Factory

Factory provides a way to encapsulate comparers creeation and configuration. Factory should implement IComparersFactory or should be inherited from ComparersFactory.

  1. public class MyComparersFactory: ComparersFactory  
  2. {  
  3.     public override IComparer<T> GetObjectsComparer<T>(ComparisonSettings settings = null, IBaseComparer parentComparer = null)  
  4.     {  
  5.         if (typeof(T) == typeof(ClassA))  
  6.         {  
  7.             var comparer = new Comparer<ClassA>(settings, parentComparer, this);  
  8.             comparer.AddComparerOverride<Guid>(new MyCustomGuidComparer());  
  9.   
  10.             return (IComparer<T>)comparer;  
  11.         }  
  12.   
  13.         return base.GetObjectsComparer<T>(settings, parentComparer);  
  14.     }  
  15. }   
Non-generic comparer

This comparer creates generic implementation of comparer for each comparison.

  1. var comparer = new Comparer();  
  2. var isEqual = comparer.Compare(a1, a2);   
Useful Value Comparers

Framework contains several custom comparers that can be useful in many cases.

DoNotCompareValueComparer

Allows us to skip some fields/types. Has singleton implementation (DoNotCompareValueComparer.Instance).

DynamicValueComparer

Receives comparison rule as a function.

NulableStringsValueComparer

Null and empty strings are considered as equal values. Has singleton implementation (NulableStringsValueComparer.Instance).

DefaultValueValueComparer

Allows to consider provided value and default value of specified type as equal values (see Example 3 below).

IgnoreCaseStringsValueComparer

Allows to compare string ignoring case. Has singleton implementation (IgnoreCaseStringsValueComparer.Instance).

Examples

There are some more complex examples how Objects Comparer can be used.

Example 1 - Expected Message Challenge

Check if received message equal to the expected message.

Problems
  • DateCreated, DateSent and DateReceived properties need to be skipped
  • Auto generated Id property need to be skipped
  • Message property of Error class need to be skipped
Solution
  1. public class Error  
  2. {  
  3.     public int Id { getset; }  
  4.   
  5.     public string Messgae { getset; }  
  6. }  
  7.   
  8. public class Message  
  9. {  
  10.     public string Id { getset; }  
  11.   
  12.     public DateTime DateCreated { getset; }  
  13.   
  14.     public DateTime DateSent { getset; }  
  15.   
  16.     public DateTime DateReceived { getset; }  
  17.   
  18.     public int MessageType { getset; }  
  19.   
  20.     public int Status { getset; }  
  21.   
  22.     public List<Error> Errors { getset; }  
  23.   
  24.     public override string ToString()  
  25.     {  
  26.         return $"Id:{Id}, Type:{MessageType}, Status:{Status}";  
  27.     }  
  28. }   

Configuring comparer

  1. _comparer = new Comparer<Message>(  
  2.     new ComparisonSettings  
  3.     {  
  4.         //Null and empty error lists are equal  
  5.         EmptyAndNullEnumerablesEqual = true  
  6.     });  
  7. //Do not compare Dates   
  8. _comparer.AddComparerOverride<DateTime>(DoNotCompareValueComparer.Instance);  
  9. //Do not compare Id  
  10. _comparer.AddComparerOverride(() => new Message().Id, DoNotCompareValueComparer.Instance);  
  11. //Do not compare Message Text  
  12. _comparer.AddComparerOverride(() => new Error().Messgae, DoNotCompareValueComparer.Instance);  
  1. var expectedMessage = new Message  
  2. {  
  3.     MessageType = 1,  
  4.     Status = 0  
  5. };  
  6.   
  7. var actualMessage = new Message  
  8. {  
  9.     Id = "M12345",  
  10.     DateCreated = DateTime.Now,  
  11.     DateSent = DateTime.Now,  
  12.     DateReceived = DateTime.Now,  
  13.     MessageType = 1,  
  14.     Status = 0  
  15. };  
  16.   
  17. IEnumerable<Difference> differences;  
  18. var isEqual = _comparer.Compare(expectedMessage, actualMessage, out differences);  
Objects are equal 
  1. var expectedMessage = new Message  
  2. {  
  3.     MessageType = 1,  
  4.     Status = 1,  
  5.     Errors = new List<Error>  
  6.     {  
  7.         new Error { Id = 2 },  
  8.         new Error { Id = 7 }  
  9.     }  
  10. };  
  11.   
  12. var actualMessage = new Message  
  13. {  
  14.     Id = "M12345",  
  15.     DateCreated = DateTime.Now,  
  16.     DateSent = DateTime.Now,  
  17.     DateReceived = DateTime.Now,  
  18.     MessageType = 1,  
  19.     Status = 1,  
  20.     Errors = new List<Error>  
  21.     {  
  22.         new Error { Id = 2, Messgae = "Some error #2" },  
  23.         new Error { Id = 7, Messgae = "Some error #7" },  
  24.     }  
  25. };  
  26.   
  27. IEnumerable<Difference> differences;  
  28. var isEqual = _comparer.Compare(expectedMessage, actualMessage, out differences);  
Objects are equal
  1. var expectedMessage = new Message  
  2. {  
  3.     MessageType = 1,  
  4.     Status = 1,  
  5.     Errors = new List<Error>  
  6.     {  
  7.         new Error { Id = 2, Messgae = "Some error #2" },  
  8.         new Error { Id = 8, Messgae = "Some error #8" }  
  9.     }  
  10. };  
  11.   
  12. var actualMessage = new Message  
  13. {  
  14.     Id = "M12345",  
  15.     DateCreated = DateTime.Now,  
  16.     DateSent = DateTime.Now,  
  17.     DateReceived = DateTime.Now,  
  18.     MessageType = 1,  
  19.     Status = 2,  
  20.     Errors = new List<Error>  
  21.     {  
  22.         new Error { Id = 2, Messgae = "Some error #2" },  
  23.         new Error { Id = 7, Messgae = "Some error #7" }  
  24.     }  
  25. };  
  26.   
  27. IEnumerable<Difference> differences;  
  28. var isEqual = _comparer.Compare(expectedMessage, actualMessage, out differences);   

Difference: DifferenceType=ValueMismatch, MemberPath='Status', Value1='1', Value2='2'.

Difference: DifferenceType=ValueMismatch, MemberPath='Errors[1].Id', Value1='8', Value2='7'.

Example 2 Persons comparison Challenge

Compare persons from different sources.

Problems
  • PhoneNumber format can be in different. Example: "111-555-8888" and "(111) 555 8888"
  • MiddleName can exist in one source but does not exist in another source. It makes a sense to compare MiddleName only if it has value in both sources.
  • PersonId property need to be skipped
Solution
  1. public class Person  
  2. {  
  3.     public Guid PersonId { getset; }  
  4.   
  5.     public string FirstName { getset; }  
  6.   
  7.     public string LastName { getset; }  
  8.   
  9.     public string MiddleName { getset; }  
  10.   
  11.     public string PhoneNumber { getset; }  
  12.   
  13.     public override string ToString()  
  14.     {  
  15.         return $"{FirstName} {MiddleName} {LastName} ({PhoneNumber})";  
  16.     }  
  17. }   

Phone number can have different formats. Let’s compare only digits.

  1. public class PhoneNumberComparer: AbstractValueComparer<string>  
  2. {  
  3.     public override bool Compare(string obj1, string obj2, ComparisonSettings settings)  
  4.     {  
  5.         return ExtractDigits(obj1) == ExtractDigits(obj2);  
  6.     }  
  7.   
  8.     private string ExtractDigits(string str)  
  9.     {  
  10.         return string.Join(  
  11.             string.Empty,   
  12.             (str ?? string.Empty)  
  13.                 .ToCharArray()  
  14.                 .Where(char.IsDigit));  
  15.     }  
  16. }   

Factory allows not to configure comparer every time we need to create it.

  1. public class MyComparersFactory: ComparersFactory  
  2. {  
  3.     public override IComparer<T> GetObjectsComparer<T>(ComparisonSettings settings = null, IBaseComparer parentComparer = null)  
  4.     {  
  5.         if (typeof(T) == typeof(Person))  
  6.         {  
  7.             var comparer = new Comparer<Person>(settings, parentComparer, this);  
  8.             //Do not compare PersonId  
  9.             comparer.AddComparerOverride<Guid>(DoNotCompareValueComparer.Instance);  
  10.             //Sometimes MiddleName can be skipped. Compare only if property has value.  
  11.             comparer.AddComparerOverride(  
  12.                 () => new Person().MiddleName,  
  13.                 (s1, s2, parentSettings) => string.IsNullOrWhiteSpace(s1) || string.IsNullOrWhiteSpace(s2) || s1 == s2);  
  14.             comparer.AddComparerOverride(  
  15.                 () => new Person().PhoneNumber,  
  16.                 new PhoneNumberComparer());  
  17.   
  18.             return (IComparer<T>)comparer;  
  19.         }  
  20.   
  21.         return base.GetObjectsComparer<T>(settings, parentComparer);  
  22.     }  
  23. }   

Configuring comparer.

  1. _factory = new MyComparersFactory();  
  2. _comparer = _factory.GetObjectsComparer<Person>();  
  1. var person1 = new Person  
  2. {  
  3.     PersonId = Guid.NewGuid(),  
  4.     FirstName = "John",  
  5.     LastName = "Doe",  
  6.     MiddleName = "F",  
  7.     PhoneNumber = "111-555-8888"  
  8. };  
  9. var person2 = new Person  
  10. {  
  11.     PersonId = Guid.NewGuid(),  
  12.     FirstName = "John",  
  13.     LastName = "Doe",  
  14.     PhoneNumber = "(111) 555 8888"  
  15. };  
  16.   
  17. IEnumerable<Difference> differences;  
  18. var isEqual = _comparer.Compare(person1, person2, out differences);  

Objects are equal

  1. var person1 = new Person  
  2. {  
  3.     PersonId = Guid.NewGuid(),  
  4.     FirstName = "Jack",  
  5.     LastName = "Doe",  
  6.     MiddleName = "F",  
  7.     PhoneNumber = "111-555-8888"  
  8. };  
  9. var person2 = new Person  
  10. {  
  11.     PersonId = Guid.NewGuid(),  
  12.     FirstName = "John",  
  13.     LastName = "Doe",  
  14.     MiddleName = "L",  
  15.     PhoneNumber = "222-555-9999"  
  16. };  
  17.   
  18. IEnumerable<Difference> differences;  
  19. var isEqual = _comparer.Compare(person1, person2, out differences);   
Difference: DifferenceType=ValueMismatch, MemberPath='FirstName', Value1='Jack', Value2='John'.

Difference: DifferenceType=ValueMismatch, MemberPath='MiddleName', Value1='F', Value2='L'.

Difference: DifferenceType=ValueMismatch, MemberPath='PhoneNumber', Value1='111-555-8888', Value2='222-555-9999'.

Example 3 Comparing JSON configuration files Challenge

There are files with settings with some differences that need to be found. Json.NET is used to deserialize JSON data.

Problems
  • URLs can be with or without HTTP prefix.
  • DataCompression is Off by default
  • SmartMode1...3 disabled by default
  • ConnectionString, Email, and Notifications need to be skipped
  • If ProcessTaskTimeout or TotalProcessTimeout settings are skipped, the default values will be used. So, if in one file, a setting does not exist and in another file, this setting has default value, it is actually the same.
Files Settings 0
  1. {  
  2.   "ConnectionString""USER ID=superuser;PASSWORD=superpassword;DATA SOURCE=localhost:1111",  
  3.   "Email": {  
  4.     "Port": 25,  
  5.     "Host""MyHost.com",  
  6.     "EmailAddress""test@MyHost.com"  
  7.   },  
  8.   "Settings": {  
  9.     "DataCompression""On",  
  10.     "DataSourceType""MultiDataSource",  
  11.     "SomeUrl""http://MyHost.com/VeryImportantData",  
  12.     "SomeOtherUrl""http://MyHost.com/NotSoImportantData/",  
  13.     "CacheMode""Memory",  
  14.     "MaxCacheSize""1GB",  
  15.     "SuperModes": {  
  16.       "SmartMode1""Enabled",  
  17.       "SmartMode2""Disabled",  
  18.       "SmartMode3""Enabled"  
  19.     }  
  20.   },  
  21.   "Timeouts": {  
  22.     "TotalProcessTimeout": 500,  
  23.     "ProcessTaskTimeout": 100  
  24.   },  
  25.   "BackupSettings": {  
  26.     "BackupIntervalUnit""Day",  
  27.     "BackupInterval": 100  
  28.   },  
  29.   "Notifications": [  
  30.     {  
  31.       "Phone""111-222-3333"  
  32.     },  
  33.     {  
  34.       "Phone""111-222-4444"  
  35.     },  
  36.     {  
  37.       "EMail""support@MyHost.com"  
  38.     }  
  39.   ],  
  40.   "Logging": {  
  41.     "Enabled"true,  
  42.     "Pattern""Logs\\MyApplication.%data{yyyyMMdd}.log",  
  43.     "MaximumFileSize""20MB",  
  44.     "Level""ALL"  
  45.   }  
  46. }   
Settings1 
  1. {  
  2.   "ConnectionString""USER ID=admin;PASSWORD=*****;DATA SOURCE=localhost:22222",  
  3.   "Email": {  
  4.     "Port": 25,  
  5.     "Host""MyHost.com",  
  6.     "EmailAddress""test@MyHost.com"  
  7.   },  
  8.   "Settings": {  
  9.     "DataCompression""On",  
  10.     "DataSourceType""MultiDataSource",  
  11.     "SomeUrl""MyHost.com/VeryImportantData",  
  12.     "SomeOtherUrl""MyHost.com/NotSoImportantData/",  
  13.     "CacheMode""Memory",  
  14.     "MaxCacheSize""1GB",  
  15.     "SuperModes": {  
  16.       "SmartMode1""enabled",  
  17.       "SmartMode3""enabled"  
  18.     }  
  19.   },  
  20.   "BackupSettings": {  
  21.     "BackupIntervalUnit""Day",  
  22.     "BackupInterval": 100  
  23.   },  
  24.   "Notifications": [  
  25.     {  
  26.       "Phone""111-222-3333"  
  27.     },  
  28.     {  
  29.       "EMail""support@MyHost.com"  
  30.     }  
  31.   ],  
  32.   "Logging": {  
  33.     "Enabled"true,  
  34.     "Pattern""Logs\\MyApplication.%data{yyyyMMdd}.log",  
  35.     "MaximumFileSize""20MB",  
  36.     "Level""ALL"  
  37.   }  
  38. }   
Settings2 
  1. {  
  2.   "ConnectionString""USER ID=superuser;PASSWORD=superpassword;DATA SOURCE=localhost:1111",  
  3.   "Email": {  
  4.     "Port": 25,  
  5.     "Host""MyHost.com",  
  6.     "EmailAddress""test@MyHost.com"  
  7.   },  
  8.   "Settings": {  
  9.     "DataSourceType""MultiDataSource",  
  10.     "SomeUrl""http://MyHost.com/VeryImportantData",  
  11.     "SomeOtherUrl""http://MyHost.com/NotSoImportantData/",  
  12.     "CacheMode""Memory",  
  13.     "MaxCacheSize""1GB",  
  14.     "SuperModes": {  
  15.       "SmartMode3""Enabled"  
  16.     }  
  17.   },  
  18.   "Timeouts": {  
  19.     "TotalProcessTimeout": 500,  
  20.     "ProcessTaskTimeout": 200  
  21.   },  
  22.   "BackupSettings": {  
  23.     "BackupIntervalUnit""Week",  
  24.     "BackupInterval": 2  
  25.   },  
  26.   "Notifications": [  
  27.     {  
  28.       "EMail""support@MyHost.com"  
  29.     }  
  30.   ],  
  31.   "Logging": {  
  32.     "Enabled"false,  
  33.     "Pattern""Logs\\MyApplication.%data{yyyyMMdd}.log",  
  34.     "MaximumFileSize""40MB",  
  35.     "Level""ERROR"  
  36.   }  
  37. }  
 Configuring comparer. 
  1. _comparer = new Comparer(new ComparisonSettings { UseDefaultIfMemberNotExist = true });  
  2. //Some fields should be ignored  
  3. _comparer.AddComparerOverride("ConnectionString", DoNotCompareValueComparer.Instance);  
  4. _comparer.AddComparerOverride("Email", DoNotCompareValueComparer.Instance);  
  5. _comparer.AddComparerOverride("Notifications", DoNotCompareValueComparer.Instance);  
  6. //Smart Modes are disabled by default. These fields are not case sensitive  
  7. var disabledByDefaultComparer = new DefaultValueValueComparer<string>("Disabled", IgnoreCaseStringsValueComparer.Instance);  
  8. _comparer.AddComparerOverride("SmartMode1", disabledByDefaultComparer);  
  9. _comparer.AddComparerOverride("SmartMode2", disabledByDefaultComparer);  
  10. _comparer.AddComparerOverride("SmartMode3", disabledByDefaultComparer);  
  11. //http prefix in URLs should be ignored  
  12. var urlComparer = new DynamicValueComparer<string>(  
  13.     (url1, url2, settings) => url1.Trim('/').Replace(@"http://"string.Empty) == url2.Trim('/').Replace(@"http://"string.Empty));  
  14. _comparer.AddComparerOverride("SomeUrl", urlComparer);  
  15. _comparer.AddComparerOverride("SomeOtherUrl", urlComparer);  
  16. //DataCompression is Off by default.  
  17. _comparer.AddComparerOverride("DataCompression"new DefaultValueValueComparer<string>("Off", NulableStringsValueComparer.Instance));  
  18. //ProcessTaskTimeout and TotalProcessTimeout fields have default values.  
  19. _comparer.AddComparerOverride("ProcessTaskTimeout"new DefaultValueValueComparer<long>(100, DefaultValueComparer.Instance));  
  20. _comparer.AddComparerOverride("TotalProcessTimeout"new DefaultValueValueComparer<long>(500, DefaultValueComparer.Instance));  
  1. var settings0Json = LoadJson("Settings0.json");  
  2. var settings0 = JsonConvert.DeserializeObject<ExpandoObject>(settings0Json);  
  3. var settings1Json = LoadJson("Settings1.json");  
  4. var settings1 = JsonConvert.DeserializeObject<ExpandoObject>(settings1Json);  
  5.   
  6. IEnumerable<Difference> differences;  
  7. var isEqual = _comparer.Compare(settings0, settings1, out differences);  
Objects are equal 
  1. var settings0Json = LoadJson("Settings0.json");  
  2. var settings0 = JsonConvert.DeserializeObject<ExpandoObject>(settings0Json);  
  3. var settings2Json = LoadJson("Settings2.json");  
  4. var settings2 = JsonConvert.DeserializeObject<ExpandoObject>(settings2Json);  
  5.   
  6. IEnumerable<Difference> differences;  
  7. var isEqual = _comparer.Compare(settings0, settings2, out differences);   

Difference: DifferenceType=ValueMismatch, MemberPath='Settings.DataCompression', Value1='On', Value2='Off'.

Difference: DifferenceType=ValueMismatch, MemberPath='Settings.SuperModes.SmartMode1', Value1='Enabled', Value2='Disabled'.

Difference: DifferenceType=ValueMismatch, MemberPath='Timeouts.ProcessTaskTimeout', Value1='100', Value2='200'.

Difference: DifferenceType=ValueMismatch, MemberPath='BackupSettings.BackupIntervalUnit', Value1='Day', Value2='Week'.

Difference: DifferenceType=ValueMismatch, MemberPath='BackupSettings.BackupInterval', Value1='100', Value2='2'.

Difference: DifferenceType=ValueMismatch, MemberPath='Logging.Enabled', Value1='True', Value2='False'.

Difference: DifferenceType=ValueMismatch, MemberPath='Logging.MaximumFileSize', Value1='20MB', Value2='40MB'.

Difference: DifferenceType=ValueMismatch, MemberPath='Logging.Level', Value1='ALL', Value2='ERROR'.

 That's it. Enjoy.