ObjectComparator 3.6.6-beta.1

This is a prerelease version of ObjectComparator.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package ObjectComparator --version 3.6.6-beta.1
                    
NuGet\Install-Package ObjectComparator -Version 3.6.6-beta.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ObjectComparator" Version="3.6.6-beta.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ObjectComparator" Version="3.6.6-beta.1" />
                    
Directory.Packages.props
<PackageReference Include="ObjectComparator" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ObjectComparator --version 3.6.6-beta.1
                    
#r "nuget: ObjectComparator, 3.6.6-beta.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ObjectComparator@3.6.6-beta.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ObjectComparator&version=3.6.6-beta.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ObjectComparator&version=3.6.6-beta.1&prerelease
                    
Install as a Cake Tool

ObjectComparator

NuGet.org Nuget Build status .NET Actions Status

ObjectComparator is a high-performance .NET library designed for deep comparison of complex objects. The library not only identifies differences, it also highlights the exact properties and values that diverge. Developers can easily configure custom comparison rules, ignore members, and fine tune the comparison pipeline to match real-world scenarios.

Table of Contents

Key Features

  • Deep member-by-member comparisons: Unravel every detail and identify even the slightest differences between complex objects.
  • Customizable rules: Define bespoke comparison criteria for properties or fields so that you remain in control of the comparison process.
  • Performance focused: Despite its comprehensive comparisons, ObjectComparator is optimized for speed and minimal allocations.
  • Friendly diagnostics: Differences are captured with paths, expected values, actual values, and optional details, making debugging straightforward.

Installation

NuGet Package Manager Console

Install-Package ObjectComparator
Install with .NET CLI
dotnet add package ObjectComparator

Getting Started

ObjectComparator targets modern .NET versions (netstandard2.1 and higher). Install the NuGet package and add using ObjectComparator; to access the extension methods. The library works seamlessly in unit tests, integration tests, and production services.

using ObjectsComparator;

var result = actual.DeeplyEquals(expected);

The returned DeepEqualityResult contains one entry per difference. When there are no differences, result.IsEmpty is true and the compared objects are considered deeply equal.

Usage Examples

Basic Comparison

Compare two Student objects and identify the differences.

var actual = new Student
{
    Name = "Alex",
    Age = 20,
    Vehicle = new Vehicle
    {
        Model = "Audi"
    },
    Courses = new[]
    {
        new Course
        {
            Name = "Math",
            Duration = TimeSpan.FromHours(4)
        },
        new Course
        {
            Name = "Liter",
            Duration = TimeSpan.FromHours(4)
        }
    }
};

var expected = new Student
{
    Name = "Bob",
    Age = 20,
    Vehicle = new Vehicle
    {
        Model = "Opel"
    },
    Courses = new[]
    {
        new Course
        {
            Name = "Math",
            Duration = TimeSpan.FromHours(3)
        },
        new Course
        {
            Name = "Literature",
            Duration = TimeSpan.FromHours(4)
        }
    }
};

var result = actual.DeeplyEquals(expected);

/*
    Path: "Student.Name":
    Expected Value :Alex
    Actually Value :Bob

    Path: "Student.Vehicle.Model":
    Expected Value :Audi
    Actually Value :Opel

    Path: "Student.Courses[0].Duration":
    Expected Value :04:00:00
    Actually Value :03:00:00

    Path: "Student.Courses[1].Name":
    Expected Value :Liter
    Actually Value :Literature
*/

Custom Strategies for Comparison

Define specific strategies for comparing properties.

var result = actual.DeeplyEquals(
    expected,
    strategy => strategy
        .Set(x => x.Vehicle.Model, (act, exp) => act.Length == exp.Length)
        .Set(x => x.Courses[1].Name, (act, exp) => act.StartsWith('L') && exp.StartsWith('L')));

/*
    Path: "Student.Name":
    Expected Value :Alex
    Actually Value :Bob

    Path: "Student.Courses[0].Duration":
    Expected Value :04:00:00
    Actually Value :03:00:00
*/

Ignoring Specific Properties or Fields

Omit certain properties or fields from the comparison.

var ignore = new[] { "Name", "Courses", "Vehicle" };
var result = actual.DeeplyEquals(expected, ignore);

/*
    Objects are deeply equal
*/

Display Distinctions with Custom Strategy

Provide specific strategies and display the differences.

var result = actual.DeeplyEquals(
    expected,
    strategy => strategy
        .Set(x => x.Vehicle.Model, (act, exp) => act.StartsWith('A') && exp.StartsWith('A')),
    "Name",
    "Courses");

/*
    Path: "Student.Vehicle.Model":
    Expected Value :Audi
    Actually Value :Opel
    Details : (act:(Audi), exp:(Opel)) => (act:(Audi).StartsWith(A) AndAlso exp:(Opel).StartsWith(A))
*/

var skip = new[] { "Vehicle", "Name", "Courses[1].Name" };
var resultWithDisplay = expected.DeeplyEquals(
    actual,
    str => str.Set(
        x => x.Courses[0].Duration,
        (act, exp) => act > TimeSpan.FromHours(3),
        new Display { Expected = "Expected that Duration should be more that 3 hours" }),
    skip);

/*
    Path: "Student.Courses[0].Duration":
    Expected Value :Expected that Duration should be more that 3 hours
    Actually Value :04:00:00
    Details : (act:(03:00:00), exp:(04:00:00)) => (act:(03:00:00) > 03:00:00)
*/

Comparison for Collection Types

Identify differences between two list or array-based collection objects, including nested structures.

var actual = new GroupPortals
{
    Portals = new List<int> { 1, 2, 3, 5 },
    Portals1 = new List<GroupPortals1>
    {
        new GroupPortals1
        {
            Courses = new List<Course>
            {
                new Course { Name = "test" }
            }
        }
    }
};

var expected = new GroupPortals
{
    Portals = new List<int> { 1, 2, 3, 4, 7, 0 },
    Portals1 = new List<GroupPortals1>
    {
        new GroupPortals1
        {
            Courses = new List<Course>
            {
                new Course { Name = "test1" }
            }
        }
    }
};

var result = expected.DeeplyEquals(actual);

/*
    Path: "GroupPortals.Portals[3]":
    Expected Value: 4
    Actual Value: 5

    Path: "GroupPortals.Portals[4]":
    Expected Value: 7
    Actual Value:
    Details: Removed

    Path: "GroupPortals.Portals[5]":
    Expected Value: 0
    Actual Value:
    Details: Removed

    Path: "GroupPortals.Portals1[0].Courses[0].Name":
    Expected Value: test1
    Actual Value: test
*/

Comparison for Dictionary Types

Identify differences between two dictionary objects.

var expected = new Library
{
    Books = new Dictionary<string, Book>
    {
        ["hobbit"] = new Book { Pages = 1000, Text = "hobbit Text" },
        ["murder in orient express"] = new Book { Pages = 500, Text = "murder in orient express Text" },
        ["Shantaram"] = new Book { Pages = 500, Text = "Shantaram Text" }
    }
};

var actual = new Library
{
    Books = new Dictionary<string, Book>
    {
        ["hobbit"] = new Book { Pages = 1, Text = "hobbit Text" },
        ["murder in orient express"] = new Book { Pages = 500, Text = "murder in orient express Text1" },
        ["Shantaram"] = new Book { Pages = 500, Text = "Shantaram Text" },
        ["Shantaram1"] = new() { Pages = 500, Text = "Shantaram Text" }
    }
};

var result = expected.DeeplyEquals(actual);

/*
    Path: "Library.Books":
    Expected Value:
    Actual Value: Shantaram1
    Details: Added

    Path: "Library.Books[hobbit].Pages":
    Expected Value: 1000
    Actual Value: 1

    Path: "Library.Books[murder in orient express].Text":
    Expected Value: murder in orient express Text
    Actual Value: murder in orient express Text1
*/

Ignore Strategy

Apply a strategy to ignore certain comparisons based on conditions.

var act = new Student
{
    Name = "StudentName",
    Age = 1,
    Courses = new[]
    {
        new Course
        {
            Name = "CourseName"
        }
    }
};

var exp = new Student
{
    Name = "StudentName1",
    Age = 1,
    Courses = new[]
    {
        new Course
        {
            Name = "CourseName1"
        }
    }
};

var distinctions = act.DeeplyEquals(exp, propName => propName.EndsWith("Name"));
/*
    Objects are deeply equal
*/

DeeplyEquals when Equals Is Overridden

var actual = new SomeTest("A");
var expected = new SomeTest("B");

var result = exp.DeeplyEquals(act);

/*
    Path: "SomeTest":
    Expected Value :ObjectsComparator.Tests.SomeTest
    Actually Value :ObjectsComparator.Tests.SomeTest
    Details : Was used override 'Equals()'
*/

DeeplyEquals when Equality Operator Is Overridden

/*
    Path: "SomeTest":
    Expected Value :ObjectsComparator.Tests.SomeTest
    Actually Value :ObjectsComparator.Tests.SomeTest
    Details : == (Equality Operator)
*/

Display Distinctions for Dictionary Types

var firstDictionary = new Dictionary<string, string>
{
    { "Key", "Value" },
    { "AnotherKey", "Value" },
};

var secondDictionary = new Dictionary<string, string>
{
    { "Key", "Value" },
    { "AnotherKey", "AnotherValue" },
};

var result = firstDictionary.DeeplyEquals(secondDictionary);

/*
    Path: "Dictionary<String, String>[AnotherKey]":
    Expected Value :Value
    Actually Value :AnotherValue
*/

Comparison for Anonymous Types

Detect differences when dealing with anonymous types.

var actual = new { Integer = 1, String = "Test", Nested = new byte[] { 1, 2, 3 } };
var expected = new { Integer = 1, String = "Test", Nested = new byte[] { 1, 2, 4 } };

var result = exp.DeeplyEquals(act);

/*
    Path: "AnonymousType<Int32, String, Byte[]>.Nested[2]":
    Expected Value :3
    Actually Value :4
*/

Comparison for Different Types

Compare objects with different types that share the same shape or property names.

var expected = new LegacyStudent
{
    Name = "Alex",
    Age = 20
};

var actual = new Student
{
    Name = "Alex",
    Age = 21
};

var result = expected.DeeplyEquals(actual, options => options.AllowDifferentTypes());

// Or use the convenience overload:
var result2 = expected.DeeplyEqualsIgnoreObjectTypes(actual);

/*
    Path: "LegacyStudent.Age":
    Expected Value: 20
    Actual Value: 21
*/

Convert Comparison Result to JSON

You can serialize the result of object comparison (DeepEqualityResult) into a structured JSON format, suitable for logging, UI display, or audits.

var distinctions = DeepEqualityResult.Create(new[]
{
    new Distinction("Snapshot.Status", "Active", "Deprecated", "Different state"),
    new Distinction("Snapshot.Rules[2].Expression", "Amount > 100", "Amount > 200"),
    new Distinction("Snapshot.Rules[6].Name", "OldName", "NewName"),
    new Distinction("Snapshot.Rules[3]", "Rule-3", "Rule-3 v2"),
    new Distinction("Snapshot.Metadata[isEnabled]", true, false),
    new Distinction("Snapshot.Metadata[range of values].Min", 10, 20),
    new Distinction(
        "Snapshot.Metadata[range of values].Bounds[1].Label", "Old bound", "New bound"),
    new Distinction("Snapshot.Portals[2]", null, 91, "Added"),
    new Distinction("Snapshot.Portals[3]", null, 101, "Added"),
    new Distinction("Snapshot.Portals[4]", 1000, null, "Removed"),
    new Distinction("Snapshot.Portals[0].Title", "Main Portal", "Main Portal v2"),
});

var json = DeepEqualsExtension.ToJson(distinctions);

/*
    {
      "Status": {
        "before": "Active",
        "after": "Deprecated",
        "details": "Different state"
      },
      "Rules": {
        "2": {
          "Expression": {
            "before": "Amount > 100",
            "after": "Amount > 200",
            "details": ""
          }
        },
        "6": {
          "Name": {
            "before": "OldName",
            "after": "NewName",
            "details": ""
          }
        },
        "3": {
          "before": "Rule-3",
          "after": "Rule-3 v2",
          "details": ""
        }
      },
      "Metadata": {
        "isEnabled": {
          "before": true,
          "after": false,
          "details": ""
        },
        "range of values": {
          "Min": {
            "before": 10,
            "after": 20,
            "details": ""
          },
          "Bounds": {
            "1": {
              "Label": {
                "before": "Old bound",
                "after": "New bound",
                "details": ""
              }
            }
          }
        }
      },
      "Portals": {
        "2": {
          "before": null,
          "after": 91,
          "details": "Added"
        },
        "3": {
          "before": null,
          "after": 101,
          "details": "Added"
        },
        "4": {
          "before": 1000,
          "after": null,
          "details": "Removed"
        },
        "0": {
          "Title": {
            "before": "Main Portal",
            "after": "Main Portal v2",
            "details": ""
          }
        }
      }
    }
*/

Configuring the Comparison Pipeline

Prefer member-by-member comparison (property-level diffs) by skipping equality-based short-circuits. This is useful when types implement ==, Equals, or IComparable, but you need detailed change tracking on each member.

internal class CourseNew3
{
    public string Name { get; set; }
    public int Duration { get; set; }

    public static bool operator ==(CourseNew3 a, CourseNew3 b) => a?.Name == b?.Name;
    public static bool operator !=(CourseNew3 a, CourseNew3 b) => !(a == b);
    public override bool Equals(object? obj) => obj is CourseNew3 other && this == other;
    public override int GetHashCode() => Name?.GetHashCode() ?? 0;
}

var actual = new CourseNew3 { Name = "Math", Duration = 5 };
var expected = new CourseNew3 { Name = "Math", Duration = 4 };

var options = ComparatorOptions.SkipStrategies(
    StrategyType.Equality,
    StrategyType.OverridesEquals,
    StrategyType.CompareTo);

var diffs = expected.DeeplyEquals(actual, options);
// diffs[0].Path == "CourseNew3.Duration"
// diffs[0].ExpectedValue == 4
// diffs[0].ActualValue == 5

Working with the Source

To build the solution locally:

dotnet restore
dotnet build

Run the included unit tests to verify changes:

dotnet test

The repository also contains performance benchmarks under PerformanceTests that can be executed to validate comparison throughput. Benchmarks typically take longer to run and may require release builds for accurate results.

Contributing

Contributions are welcome! If you encounter an issue, have a question, or would like to suggest an improvement:

  1. Search the existing issues to avoid duplicates.
  2. Open a new issue with as much detail as possible (include sample objects, expected output, and actual output when relevant).
  3. Fork the repository, create a feature branch, and submit a pull request with your changes.

Please ensure that new code is accompanied by tests and documentation updates where applicable.

License

This project is licensed under the Apache-2.0 License. See the LICENSE file for more details.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on ObjectComparator:

Package Downloads
Twileloop.SessionGuard

Gives you a reactive event based state management framework that centralises application state and give callbacks on state updates across application. It also allows you to write and read persistant states from an XML encoded defalate compressed file, an ideal solution for having data files

Twileloop.SST

A central state management library with features like state history, diffing, undo, redo, and deep state cloning.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.6.6-beta.4 0 1/15/2026
3.6.6-beta.3 3 1/14/2026
3.6.6-beta.2 41 1/11/2026
3.6.6-beta.1 46 1/7/2026
3.6.5 5,849 10/23/2025
3.6.4 1,104 10/7/2025
3.6.3 343 10/2/2025
3.6.2 361 10/1/2025
3.6.1 9,045 4/25/2025
3.6.0 1,516 4/9/2025
3.5.9 3,927 3/9/2025
3.5.9-preview8 222 4/6/2025
3.5.9-preview7 222 3/22/2025
3.5.9-preview6 224 3/22/2025
3.5.9-preview5 214 3/18/2025
3.5.9-preview4 229 3/17/2025
3.5.9-preview3 198 3/12/2025
3.5.9-preview2 223 3/12/2025
3.5.9-preview1 226 3/12/2025
3.5.8 18,194 2/12/2024
3.5.7 6,475 10/9/2023
3.5.6 653 9/27/2023
3.5.5 908 9/16/2023
3.5.4 604 9/16/2023
3.5.3 1,710 7/31/2023
3.5.2 21,410 5/16/2022
3.5.1 1,111 5/14/2022
3.5.0 105,509 4/22/2021
3.4.0 10,083 1/8/2021
3.3.0 1,186 1/8/2021
3.2.0 1,166 12/29/2020
3.1.0 1,309 10/6/2020
3.0.0.6 1,227 5/14/2022 3.0.0.6 is deprecated.
3.0.0 1,445 9/30/2020
2.14.0 1,209 9/9/2020
2.13.0 1,568 4/15/2020
2.12.0 1,530 12/30/2019
2.11.0 4,426 4/15/2019
2.10.0 1,358 4/9/2019
2.0.9 1,370 3/21/2019
2.0.8 1,475 1/11/2019
2.0.7 1,545 12/15/2018
2.0.6 1,528 11/13/2018
2.0.5 1,470 11/12/2018
2.0.4 1,470 11/12/2018
2.0.3 1,542 11/6/2018

Added opt‑in support for deep comparison across different runtime types with ComparatorOptions.AllowDifferentTypes(true).
New DeeplyEqualsIgnoreObjectTypes convenience overload for cross‑type comparisons.
Cross‑type comparisons now work for collections and dictionaries (element/value comparisons by matching members).
Ignore‑path handling updated to respect the selected root type when comparing different types.