ObjectsComparer

repository·master·Indexed 18 days ago

https://github.com/valerat1982/objectscomparer

A C# framework for deep, recursive object-to-object comparison. It supports complex nested structures, collections (arrays, sets, multidimensional and jagged arrays), and dynamic objects such as ExpandoObject and custom DynamicObject implementations. The framework provides tools to identify differences via Difference objects, define custom comparison rules through IValueComparer<T>, ignore specific members, and configure behavior using ComparisonSettings.

Tokens
6.1K
Snippets
18
Records
20
Agent score
13%

What's inside ObjectsComparer

  1. Compare ExpandoObject (Dynamic)

    master

    You can compare ExpandoObject instances using the parameterless Comparer() constructor. The framework detects missing members and type mismatches across dynamic properties.

    Configuration: By default, if a member exists in one object but not the other, it is reported as MissedMemberInSecondObject or MissedMemberInFirstObject. You can change this behavior using ComparisonSettings:

    Set UseDefaultIfMemberNotExist = true to treat missing members as having a default value instead of reporting them as missing.

    // Default behavior
    dynamic a1 = new ExpandoObject();
    a1.Field1 = "A";
    dynamic a2 = new ExpandoObject();
    a2.Field1 = "B";
    var comparer = new Comparer();
    
    // Using ComparisonSettings to treat missing members as default values
    dynamic a1 = new ExpandoObject();
    a1.Field1 = "A";
    a1.Field2 = 0;
    dynamic a2 = new ExpandoObject();
    a2.Field1 = "B";
    a2.Field4 = "S";
    var comparer = new Comparer(new ComparisonSettings { UseDefaultIfMemberNotExist = true });
  2. Use a ComparersFactory to manage complex comparer configurations

    master

    For large projects, instead of configuring a Comparer manually every time, implement a ComparersFactory (inheriting from ComparersFactory). This allows you to centralize the logic for creating specialized comparers for specific types, including custom overrides and custom value comparers.

    public class MyComparersFactory : ComparersFactory
    {
        public override IComparer<T> GetObjectsComparer<T>(ComparisonSettings settings = null, IBaseComparer parentComparer = null)
        {
            if (typeof(T) == typeof(Person))
            {
                var comparer = new Comparer<Person>(settings, parentComparer, this);
                // Apply specific overrides for Person
                comparer.AddComparerOverride(() => new Person().PhoneNumber, new PhoneNumberComparer());
                return (IComparer<T>)comparer;
            }
    
            return base.GetObjectsComparer<T>(settings, parentComparer);
        }
    }
    
    // Usage
    _factory = new MyComparersFactory();
    _comparer = _factory.GetObjectsComparer<Person>();
  3. Compare DynamicObject implementations

    master

    To use ObjectsComparer with a custom class inheriting from DynamicObject, you must correctly override the GetDynamicMemberNames() method. This method allows the comparer to discover the dynamic properties available on the object.

    If GetDynamicMemberNames() is not implemented correctly, the comparer will not be able to traverse the dynamic members.

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        return _dictionary.Keys;
    }
  4. Configure comparison settings for enumerables

    master

    When using ComparisonSettings, you can control how collections (enumerables) are treated. Setting EmptyAndNullEnumerablesEqual = true ensures that a null list and an empty list are considered equal during comparison.

    _comparer = new Comparer<Message>(new ComparisonSettings
    {
        EmptyAndNullEnumerablesEqual = true
    });
  5. Configure ComparisonSettings

    master

    The Comparer constructor accepts an optional ComparisonSettings object to control comparison behavior.

    Available Settings

    • RecursiveComparison (default: true): If true, non-primitive types that do not have a custom comparison rule or implement IComparable will be compared using the same rules as the root objects.
    • EmptyAndNullEnumerablesEqual (default: false): If true, null values and empty enumerables (arrays, lists, etc.) are considered equal.
    • UseDefaultIfMemberNotExist (default: false): Applicable for dynamic type comparisons. If true, a missing member is considered equal to the default value of the opposite member's type.

    Custom Settings Storage

    ComparisonSettings can also act as a key-value store for custom data used within your custom comparers:

    • SetCustomSetting<T>(T value, string key = null)
    • GetCustomSetting<T>(string key = null)
  6. Compare multidimensional arrays

    master

    The framework supports multidimensional arrays (e.g., int[,]) and jagged arrays (e.g., int[][]).

    For multidimensional arrays, mismatches can occur in:

    • Dimension: If the dimensions themselves differ.
    • [i,j]: Specific coordinate mismatches.

    For jagged arrays, mismatches can occur in:

    • Length: If the inner arrays have different lengths.
    • [i][j]: Specific element mismatches.
    // Jagged array
    var a1 = new[] { new[] { 1, 2 } };
    var a2 = new[] { new[] { 1, 3 } };
    var comparer = new Comparer<int[][]>();
    // Result: Difference: DifferenceType=ValueMismatch, MemberPath='[0][1]', Value1='2', Value2='3'.
    
    // Multidimensional array
    var a1 = new[,] { { 1, 2 } };
    var a2 = new[,] { { 1, 3 } };
    var comparer = new Comparer<int[,]>();
    // Result: Difference: DifferenceType=ValueMismatch, MemberPath='[0,1]', Value1='2', Value2='3'.
  7. Compare Compiler Generated (Anonymous) Objects

    master

    When comparing anonymous types, the framework treats them as the same type only if they have the exact same set of members (same names and same types).

    Important: If the anonymous objects have different sets of members, you must cast them to (object) before passing them to Compare. If you skip this cast when the types are different, a RuntimeBinderException will be thrown.

    dynamic a1 = new { Field1 = "A", Field2 = 5 };
    dynamic a2 = new { Field1 = "B", Field2 = 8 };
    var comparer = new Comparer();
    
    // Cast to (object) to avoid RuntimeBinderException if types differ
    IEnumerable<Difference> differences;
    var isEqual = comparer.Compare((object)a1, (object)a2, out differences);
  8. Compare enumerables and arrays

    master

    The framework supports comparing arrays and enumerables. It detects differences in length and mismatches at specific indices (e.g., MemberPath='[1]').

    Common DifferenceType values for collections include:

    • ValueMismatch: Values at the same index differ.
    • TypeMismatch: Values at the same index have different types.
    • Length: The collection lengths do not match.
    // Array comparison
    var a1 = new[] { 1, 2, 3 };
    var a2 = new[] { 1, 4, 3 };
    var comparer = new Comparer<int[]>();
    // Result: Difference: DifferenceType=ValueMismatch, MemberPath='[1]', Value1='2', Value2='4'.
    
    // ArrayList comparison
    var a1 = new ArrayList { "Str1", "Str2" };
    var a2 = new ArrayList { "Str1", 5 };
    var comparer = new Comparer<ArrayList>();
    // Result: Difference: DifferenceType=TypeMismatch, MemberPath='[1]', Value1='Str2', Value2='5'.
  9. Compare Sets (HashSet)

    master

    When comparing sets like HashSet<T>, the framework identifies elements that exist in one set but are missing from the other using the following DifferenceType values:

    • MissedElementInSecondObject: Element exists in the first set but not the second.
    • MissedElementInFirstObject: Element exists in the second set but not the first.
    var a1 = new HashSet<int> { 1, 2, 3 };
    var a2 = new HashSet<int> { 2, 1, 4 };
    var comparer = new Comparer<HashSet<int>>();
    // Result: 
    // Difference: DifferenceType=MissedElementInSecondObject, MemberPath='', Value1='3', Value2=''.
    // Difference: DifferenceType=MissedElementInFirstObject, MemberPath='', Value1='', Value2='4'.
  10. Compare dynamic objects (e.g., JSON deserialized to ExpandoObject)

    master

    When comparing dynamic objects like those produced by JsonConvert.DeserializeObject<ExpandoObject>(json), use the parameterless Comparer constructor and provide overrides using string-based property names.

    // Use UseDefaultIfMemberNotExist to handle missing keys gracefully
    _comparer = new Comparer(new ComparisonSettings { UseDefaultIfMemberNotExist = true });
    
    // Override by string name for dynamic properties
    _comparer.AddComparerOverride("ConnectionString", DoNotCompareValueComparer.Instance);
    
    // Use custom logic for specific dynamic fields
    var urlComparer = new DynamicValueComparer<string>(
        (url1, url2, settings) => url1.Trim('/').Replace(@"http://", string.Empty) == url2.Trim('/').Replace(@"http://", string.Empty));
    _comparer.AddComparerOverride("SomeUrl", urlComparer);
    
    // Compare the ExpandoObjects
    var isEqual = _comparer.Compare(settings0, settings1, out var differences);
  11. Implement a custom collection comparer with identity matching

    master

    By default, the comparer treats lists as different if their counts differ. To compare lists of items by their content (even if counts differ), implement AbstractComparer<T> (where T is the list type).

    Inside CalculateDifferences, you can manually iterate through items, find matches based on a unique identifier (like an Id), and use a nested IComparer to compare the matched items. This allows you to report differences at specific paths like Items[Id=1].Property.

    public class CustomFormulaItemsComparer : AbstractComparer<IList<FormulaItem>>
    {
        public override IEnumerable<Difference> CalculateDifferences(IList<FormulaItem> obj1, IList<FormulaItem> obj2)
        {
            // ... handle nulls and count mismatches ...
    
            foreach (var formulaItem in obj1)
            {
                // Find matching item in second list by ID
                var formulaItem2 = obj2.FirstOrDefault(fi => fi.Id == formulaItem.Id);
    
                if (formulaItem2 != null)
                {
                    // Use a standard comparer for the individual items
                    var comparer = Factory.GetObjectsComparer<FormulaItem>();
                    foreach (var difference in comparer.CalculateDifferences(formulaItem, formulaItem2))
                    {
                        // Insert the ID into the path for clarity
                        yield return difference.InsertPath($"[Id={formulaItem.Id}]");
                    }
                }
            }
        }
    }