Objects Comparer In .NET

Introduction

 
Objects Comparer is an object-to-object comparer that allows you to compare objects recursively member by member and defines custom comparison rules for certain properties, fields, or types.
 
Objects Comparer can be considered as ready to use a framework or as an idea for similar solutions. This article focuses on using a framework than on implementation. If you are interested in implementation, modification, or you have any ideas on how to make this framework better, please feel free to contact me.
 
Installation
 
Objects Comparer Framework can be installed as a NuGet package or downloaded from GitHub.
 
Install-Package ObjectsComparer
 
 
Basic Example 
  1. public class ClassA {  
  2.     public string StringProperty {  
  3.         get;  
  4.         set;  
  5.     }  
  6.     public int IntProperty {  
  7.         get;  
  8.         set;  
  9.     }  
  10. }  
  11. var a1 = new ClassA {  
  12.     StringProperty = "String", IntProperty = 1  
  13. };  
  14. var a2 = new ClassA {  
  15.     StringProperty = "String", IntProperty = 1  
  16. };  
  17. var comparer = new Comparer < ClassA > ();  
  18. var isEqual = comparer.Compare(a1, a2);  
  19. Debug.WriteLine("a1 and a2 are " + (isEqual ? "equal" : "not equal"));  
  20. a1 and a2 are equal  
  21. var a1 = new ClassA {  
  22.     StringProperty = "String", IntProperty = 1  
  23. };  
  24. var a2 = new ClassA {  
  25.     StringProperty = "String", IntProperty = 2  
  26. };  
  27. var comparer = new Comparer < ClassA > ();  
  28. IEnumerable < Difference > differenses;  
  29. var isEqual = comparer.Compare(a1, a2, out differenses);  
  30. var differensesList = differenses.ToList();  
  31. Debug.WriteLine("a1 and a2 are " + (isEqual ? "equal" : "not equal"));  
  32. if (!isEqual) {  
  33.     Debug.WriteLine("Differences:");  
  34.     Debug.WriteLine(string.Join(Environment.NewLine, differensesList));  
  35. }  
a1 and a2 are not equal.
 
Differences
 
Difference: MemberPath='IntProperty', Value1='1', Value2='2'
 
Comparison Settings
 
The framework provides some useful comparison settings.
 
RecursiveComparison is true by default.
 
EmptyAndNullEnumerablesEqual is 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)  
Overriding comparison rules
 
Comparer should be inherited from AbstractValueComparer<T> or should implement IValueComparer<T> .
  1. public class MyComparer: AbstractValueComparer < string > {  
  2.     public override bool Compare(string obj1, string obj2, ComparisonSettings settings) {  
  3.         return obj1 == obj2; //Implement comparison logic here  
  4.     }  
  5. }  
Type comparison rule override. 
  1. comparer.AddComparerOverride<string>(new MyComparer());  
Field comparison rule override could be done in three different ways.
  1. comparer.AddComparerOverride(() => new ClassA().StringProperty, new MyComparer());  
  2. comparer.AddComparerOverride(  
  3.     () => new ClassA().StringProperty, (s1, s2, parentSettings) => s1 == s2, s => s.ToString());  
  4. comparer.AddComparerOverride(  
  5.     () => new ClassA().StringProperty, (s1, s2, parentSettings) => s1 == s2);  
Factory
 
Factory should implement IComparersFactory or should be inherited from ComparersFactory. 
 
  1. public class MyComparersFactory: ComparersFactory {  
  2.     public override IComparer < T > GetObjectsComparer < T > (ComparisonSettings settings = null, IBaseComparer parentComparer = null) {  
  3.         if (typeof(T) == typeof(ClassA)) {  
  4.             var comparer = new Comparer < ClassA > (settings, parentComparer, this);  
  5.             comparer.AddComparerOverride < Guid > (new MyCustomGuidComparer());  
  6.             return (IComparer < T > ) comparer;  
  7.         }  
  8.         return base.GetObjectsComparer < T > (settings, parentComparer);  
  9.     }  
  10. }  
Non-generic comparer
  1. var comparer = new Comparer<ClassA>();  
  2. var isEqual = comparer.Compare(a1, a2);  
This comparer creates generic implementation of comparer for each comparison.
 
Useful Value Comparers
 
There are some custom comparers that can be useful.
 
DoNotCompareValueComparer. Use it to skip some fields/types. There is a singleton implementation.(DoNotCompareValueComparer.Instance).
 
DynamicValueComparer<T>. Receives comparison rule as a constructor parameter.
 
NulableStringsValueComparer. Null and empty strings are equal.
 
Examples
 
There are some examples of how Objects Comparer can be used.
 
NSubstitute is used for developing unit tests.
 
Example 1 Expected Message
 
Challenge: Check if the received message is equal to the expected message or not.
  1. public class Error {  
  2.     public int Id {  
  3.         get;  
  4.         set;  
  5.     }  
  6.     public string Messgae {  
  7.         get;  
  8.         set;  
  9.     }  
  10. }  
  11. public class Message {  
  12.     public string Id {  
  13.         get;  
  14.         set;  
  15.     }  
  16.     public DateTime DateCreated {  
  17.         get;  
  18.         set;  
  19.     }  
  20.     public int MessageType {  
  21.         get;  
  22.         set;  
  23.     }  
  24.     public int Status {  
  25.         get;  
  26.         set;  
  27.     }  
  28.     public List < Error > Errors {  
  29.         get;  
  30.         set;  
  31.     }  
  32.     public override string ToString() {  
  33.         return $ "Id:{Id}, Date:{DateCreated}, Type:{MessageType}, Status:{Status}";  
  34.     }  
  35. }  
  36. [TestFixture]  
  37. public class Example1Tests {  
  38.     private IComparer < Message > _comparer;  
  39.     [SetUp]  
  40.     public void SetUp() {  
  41.             _comparer = new Comparer < Message > (new ComparisonSettings {  
  42.                 //Null and empty error lists are equal  
  43.                 EmptyAndNullEnumerablesEqual = true  
  44.             });  
  45.             //Do not compare DateCreated  
  46.             _comparer.AddComparerOverride < DateTime > (DoNotCompareValueComparer.Instance);  
  47.             //Do not compare Id  
  48.             _comparer.AddComparerOverride(() => new Message().Id, DoNotCompareValueComparer.Instance);  
  49.             //Do not compare Message Text  
  50.             _comparer.AddComparerOverride(() => new Error().Messgae, DoNotCompareValueComparer.Instance);  
  51.         }  
  52.         [Test]  
  53.     public void EqualMessagesWithoutErrorsTest() {  
  54.             var expectedMessage = new Message {  
  55.                 MessageType = 1,  
  56.                     Status = 0,  
  57.             };  
  58.             var actualMessage = new Message {  
  59.                 Id = "M12345",  
  60.                     DateCreated = DateTime.Now,  
  61.                     MessageType = 1,  
  62.                     Status = 0,  
  63.             };  
  64.             var isEqual = _comparer.Compare(expectedMessage, actualMessage);  
  65.             Assert.IsTrue(isEqual);  
  66.         }  
  67.         [Test]  
  68.     public void EqualMessagesWithErrorsTest() {  
  69.         var expectedMessage = new Message {  
  70.             MessageType = 1,  
  71.                 Status = 1,  
  72.                 Errors = new List < Error > {  
  73.                     new Error {  
  74.                         Id = 2  
  75.                     },  
  76.                     new Error {  
  77.                         Id = 7  
  78.                     }  
  79.                 }  
  80.         };  
  81.         var actualMessage = new Message {  
  82.             Id = "M12345",  
  83.                 DateCreated = DateTime.Now,  
  84.                 MessageType = 1,  
  85.                 Status = 1,  
  86.                 Errors = new List < Error > {  
  87.                     new Error {  
  88.                         Id = 2, Messgae = "Some error #2"  
  89.                     },  
  90.                     new Error {  
  91.                         Id = 7, Messgae = "Some error #7"  
  92.                     },  
  93.                 }  
  94.         };  
  95.         var isEqual = _comparer.Compare(expectedMessage, actualMessage);  
  96.         Assert.IsTrue(isEqual);  
  97.     }  
  98. }  
Example 2 Person comparison
 
Challenge: Compare persons from different sources.
  1. public class Person {  
  2.     public Guid PersonId {  
  3.         get;  
  4.         set;  
  5.     }  
  6.     public string FirstName {  
  7.         get;  
  8.         set;  
  9.     }  
  10.     public string LastName {  
  11.         get;  
  12.         set;  
  13.     }  
  14.     public string MiddleName {  
  15.         get;  
  16.         set;  
  17.     }  
  18.     public string PhoneNumber {  
  19.         get;  
  20.         set;  
  21.     }  
  22.     public override string ToString() {  
  23.         return $ "{FirstName} {MiddleName} {LastName} ({PhoneNumber})";  
  24.     }  
  25. }  
Phone number can have different formats. Let’s compare only digits.
  1. public class PhoneNumberComparer: AbstractValueComparer < string > {  
  2.     public override bool Compare(string obj1, string obj2, ComparisonSettings settings) {  
  3.         return ExtractDigits(obj1) == ExtractDigits(obj2);  
  4.     }  
  5.     private string ExtractDigits(string str) {  
  6.         return string.Join(string.Empty, (str ? ? string.Empty).ToCharArray().Where(char.IsDigit));  
  7.     }  
  8. }  
Factory allows not to configure comparer every time, we need to create it.
  1. public class MyComparersFactory: ComparersFactory {  
  2.         public override IComparer < T > GetObjectsComparer < T > (ComparisonSettings settings = null, IBaseComparer parentComparer = null) {  
  3.             if (typeof(T) == typeof(Person)) {  
  4.                 var comparer = new Comparer < Person > (settings, parentComparer, this);  
  5.                 //Do not compare PersonId  
  6.                 comparer.AddComparerOverride < Guid > (DoNotCompareValueComparer.Instance);  
  7.                 //Sometimes MiddleName can be skipped. Compare only if property has value.  
  8.                 comparer.AddComparerOverride(  
  9.                     () => new Person().MiddleName, (s1, s2, parentSettings) => string.IsNullOrWhiteSpace(s1) || string.IsNullOrWhiteSpace(s2) || s1 == s2);  
  10.                 comparer.AddComparerOverride(  
  11.                     () => new Person().PhoneNumber, new PhoneNumberComparer());  
  12.                 return (IComparer < T > ) comparer;  
  13.             }  
  14.             return base.GetObjectsComparer < T > (settings, parentComparer);  
  15.         }  
  16.     }  
  17.     [TestFixture]  
  18. public class Example2Tests {  
  19.     private MyComparersFactory _factory;  
  20.     private IComparer < Person > _comparer;  
  21.     [SetUp]  
  22.     public void SetUp() {  
  23.             _factory = new MyComparersFactory();  
  24.             _comparer = _factory.GetObjectsComparer < Person > ();  
  25.         }  
  26.         [Test]  
  27.     public void EqualPersonsTest() {  
  28.             var person1 = new Person {  
  29.                 PersonId = Guid.NewGuid(),  
  30.                     FirstName = "John",  
  31.                     LastName = "Doe",  
  32.                     MiddleName = "F",  
  33.                     PhoneNumber = "111-555-8888"  
  34.             };  
  35.             var person2 = new Person {  
  36.                 PersonId = Guid.NewGuid(),  
  37.                     FirstName = "John",  
  38.                     LastName = "Doe",  
  39.                     PhoneNumber = "(111) 555 8888"  
  40.             };  
  41.             IEnumerable < Difference > differenses;  
  42.             var isEqual = _comparer.Compare(person1, person2, out differenses);  
  43.             Assert.IsTrue(isEqual);  
  44.             Debug.WriteLine($ "Persons {person1} and {person2} are equal");  
  45.         }  
  46.         [Test]  
  47.     public void DifferentPersonsTest() {  
  48.         var person1 = new Person {  
  49.             PersonId = Guid.NewGuid(),  
  50.                 FirstName = "Jack",  
  51.                 LastName = "Doe",  
  52.                 MiddleName = "F",  
  53.                 PhoneNumber = "111-555-8888"  
  54.         };  
  55.         var person2 = new Person {  
  56.             PersonId = Guid.NewGuid(),  
  57.                 FirstName = "John",  
  58.                 LastName = "Doe",  
  59.                 MiddleName = "L",  
  60.                 PhoneNumber = "222-555-9999"  
  61.         };  
  62.         IEnumerable < Difference > differenses;  
  63.         var isEqual = _comparer.Compare(person1, person2, out differenses);  
  64.         var differensesList = differenses.ToList();  
  65.         Assert.IsFalse(isEqual);  
  66.         Assert.AreEqual(3, differensesList.Count);  
  67.         Assert.IsTrue(differensesList.Any(d => d.MemberPath == "FirstName" && d.Value1 == "Jack" && d.Value2 == "John"));  
  68.         Assert.IsTrue(differensesList.Any(d => d.MemberPath == "MiddleName" && d.Value1 == "F" && d.Value2 == "L"));  
  69.         Assert.IsTrue(differensesList.Any(d => d.MemberPath == "PhoneNumber" && d.Value1 == "111-555-8888" && d.Value2 == "222-555-9999"));  
  70.         Debug.WriteLine($ "Persons {person1} and {person2}");  
  71.         Debug.WriteLine("Differences:");  
  72.         Debug.WriteLine(string.Join(Environment.NewLine, differensesList));  
  73.     }  
  74. }  
Persons John F Doe (111-555-8888) and John Doe ((111) 555 8888) are equal while the persons' Jack F Doe (111-555-8888) and John L Doe (222-555-9999) are not
 
Differences
 
Difference: MemberPath='FirstName', Value1='Jack', Value2='John'.
Difference: MemberPath='MiddleName', Value1='F', Value2='L'.
Difference: MemberPath='PhoneNumber', Value1='111-555-8888', Value2='222-555-9999'.