DeepEqual.Generator 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package DeepEqual.Generator --version 1.0.0
                    
NuGet\Install-Package DeepEqual.Generator -Version 1.0.0
                    
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="DeepEqual.Generator" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DeepEqual.Generator" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="DeepEqual.Generator" />
                    
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 DeepEqual.Generator --version 1.0.0
                    
#r "nuget: DeepEqual.Generator, 1.0.0"
                    
#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 DeepEqual.Generator@1.0.0
                    
#: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=DeepEqual.Generator&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=DeepEqual.Generator&version=1.0.0
                    
Install as a Cake Tool

DeepEqual.Generator

A C# source generator that creates super-fast, allocation-free deep equality comparers for your classes and structs.

Stop writing Equals by hand. Stop serializing to JSON just to compare objects. Just add an attribute, and you get a complete deep comparer generated at compile time.


✨ Why use this?

  • Simple – annotate your models, and you’re done.
  • Flexible – opt-in options for unordered collections, numeric tolerances, string case sensitivity, custom comparers.

⚡ Why is it faster than handwritten code?

  • Compile-time codegen: the comparer is emitted at build time as optimized IL — no reflection, no runtime expression building.
  • Direct member access: it expands equality checks into straight-line code instead of generic loops or helper calls.
  • No allocations: avoids closures, iterators, or boxing that sneak into LINQ or naive implementations.

Result: consistently 5–7× faster than hand-written comparers, with fewer allocations.


🛡️ Why is it more robust?

  • Covers corner cases: handles nested collections, dictionaries, sets, polymorphism, and reference cycles without special-casing in user code.
  • Deterministic: guarantees the same behavior across types and shapes — no surprises when you add or reorder fields.
  • Safer than manual: no risk of forgetting a property or comparing the wrong shape.

In short: you get the performance of hand-tuned code, but with the coverage of a well-tested library — and without the runtime overhead.


🚀 Getting started

Install the NuGet package:

dotnet add package DeepEqual.Generator

Annotate your type:

using DeepEqual.Generator.Shared;

[DeepComparable]
public sealed class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

At compile time, a static helper is generated:

PersonDeepEqual.AreDeepEqual(personA, personB);

🔍 What gets compared?

  • Primitives & enums – by value.
  • Strings – configurable (ordinal, ignore case, culture aware).
  • DateTime / DateTimeOffset – strict: both the Kind/Offset and Ticks must match.
  • Guid, TimeSpan, DateOnly, TimeOnly – by value.
  • Nullable<T> – compared only if both have a value.
  • Arrays & collections – element by element.
  • Dictionaries – key/value pairs deeply compared.
  • Jagged & multidimensional arrays – handled correctly.
  • Object properties – compared polymorphically if the runtime type has a generated helper.
  • Dynamics / ExpandoObject – compared as dictionaries of keys/values.
  • Cycles – supported (can be turned off if you know your graph has no cycles).

🎛 Options

On the root type

[DeepComparable(OrderInsensitiveCollections = true, IncludeInternals = true, IncludeBaseMembers = true)]
public sealed class Order { … }

Defaults:

  • OrderInsensitiveCollectionsfalse
  • IncludeInternalsfalse
  • IncludeBaseMemberstrue
  • CycleTrackingtrue

On individual members or types

public sealed class Person
{
    [DeepCompare(Kind = CompareKind.Shallow)]
    public Address? Home { get; set; }

    [DeepCompare(OrderInsensitive = true)]
    public List<string>? Tags { get; set; }

    [DeepCompare(IgnoreMembers = new[] { "CreatedAt", "UpdatedAt" })]
    public AuditInfo Info { get; set; } = new();
}

Defaults:

  • KindDeep
  • OrderInsensitivefalse
  • Members → empty (all members included)
  • IgnoreMembers → empty
  • ComparerType → null (no custom comparer)
  • KeyMembers → empty (no key-based matching)

📚 Ordered vs Unordered collections

By default, collections are compared in order. That means element by element, position matters:

var a = new[] { 1, 2, 3 };
var b = new[] { 3, 2, 1 };

If you want a collection to be compared ignoring order (treating it like a bag or set), you can:

  • Enable it globally for the type:
[DeepComparable(OrderInsensitiveCollections = true)]
public sealed class OrderBatch { public List<int> Ids { get; set; } = new(); }
  • Or mark specific members:
public sealed class TagSet
{
    [DeepCompare(OrderInsensitive = true)]
    public List<string> Tags { get; set; } = new();
}
  • Or let the element type decide:
[DeepComparable(OrderInsensitiveCollections = true)]
public sealed class Tag { public string Name { get; set; } = ""; }

public sealed class TagHolder { public List<Tag> Tags { get; set; } = new(); }

Key-based matching

For unordered collections of objects, you can mark certain properties as keys:

[DeepCompare(KeyMembers = new[] { "Id" })]
public sealed class Customer { public string Id { get; set; } = ""; public string Name { get; set; } = ""; }

Now two List<Customer> collections are equal if they contain the same customers by Id, regardless of order.


⚡ Numeric & string options

var opts = new ComparisonOptions
{
    FloatEpsilon = 0f,
    DoubleEpsilon = 0d,
    DecimalEpsilon = 0m,
    TreatNaNEqual = false,
    StringComparison = StringComparison.Ordinal
};

Defaults are strict equality for numbers and case-sensitive ordinal for strings.


🌀 Cycles

Cyclic graphs are handled safely:

[DeepComparable]
public sealed class Node
{
    public string Id { get; set; } = "";
    public Node? Next { get; set; }
}

var a = new Node { Id = "a" };
var b = new Node { Id = "a" };
a.Next = a;
b.Next = b;

NodeDeepEqual.AreDeepEqual(a, b); 

📊 Benchmarks

This document summarizes benchmark results comparing different approaches to deep object equality in .NET.
Our source-generated comparer is listed first, followed by manual implementations and popular libraries.


🏆 Generated Comparer (this project)

Scenario Time (ms) Allocations
Equal 0.0003 120 B
NotEqual (Shallow) 0.000004 0
NotEqual (Deep) 0.0003 120 B

✅ Fastest overall across equality and deep inequality checks
✅ Minimal allocations
✅ Beats manual implementations by 5–7× on deep checks
✅ Outperforms libraries and JSON-based approaches by orders of magnitude

⚠️ Note: For shallow inequality (quick “not equal” exit), handwritten code is still faster (fractions of a microsecond), but the difference is negligible in practice.


✍️ Manual Implementations

Hand-written (non-LINQ)

Scenario Time (ms) Allocations
Equal 0.0016 3,264 B
NotEqual (Shallow) 0.000001 0
NotEqual (Deep) 0.0016 3,264 B

Hand-written (LINQ style)

Scenario Time (ms) Allocations
Equal 0.0021 3,504 B
NotEqual (Shallow) 0.000001 0
NotEqual (Deep) 0.0021 3,504 B

⚠️ Slower than generated by 5–8× in deep checks, with significantly more allocations.
⚠️ Shallow inequality is slightly faster than generated, but only by fractions of a microsecond.


📦 JSON Serialization Approaches

Newtonsoft.Json

Scenario Time (ms) Allocations
Equal 1,613.124 2,035,568 B
NotEqual (Shallow) 1,477.597 2,032,768 B
NotEqual (Deep) 1,664.072 2,035,568 B

System.Text.Json

Scenario Time (ms) Allocations
Equal 1,401.291 1,398,629 B
NotEqual (Shallow) 752.367 428,893 B
NotEqual (Deep) 1,385.706 1,368,765 B

⚠️ Thousands of times slower than generated/manual.
⚠️ Huge allocations (MBs per comparison).
❌ Only useful for debugging or one-off checks, not performance-critical paths.


🔍 Library Comparers

Compare-Net-Objects

Scenario Time (ms) Allocations
Equal 2,099.460 3,418,352 B
NotEqual (Shallow) 0.002 4,728 B
NotEqual (Deep) 2,060.454 3,352,279 B

ObjectsComparer

Scenario Time (ms) Allocations
Equal 13,526.608 13,932,553 B
NotEqual (Shallow) 0.002 2,208 B
NotEqual (Deep) 12,964.030 13,552,951 B

FluentAssertions

Scenario Time (ms) Allocations
Equal 10,817.864 21,793,862 B
NotEqual (Shallow) 11,609.765 21,891,734 B
NotEqual (Deep) 11,488.218 21,921,875 B

⚠️ 10–50 seconds per 1,000 calls.
⚠️ Allocations in tens of MBs.
✅ Great for unit tests (readability), but unsuitable for production performance.


📊 Takeaways

  • Generated comparer is the clear winner:
    • Sub-millisecond performance for deep equality
    • Near-zero allocations
    • Outperforms manual and library approaches by wide margins
  • Manual comparers are OK for small/shallow checks, and win slightly on trivial “not equal” cases — but the difference is negligible compared to their cost in deep checks.
  • JSON and library-based solutions are magnitudes slower and consume massive memory.
  • FluentAssertions / ObjectsComparer / Compare-Net-Objects are best kept for testing and diagnostics, not runtime paths.

✅ When to use

  • Large object graphs (domain models, caches, trees).
  • Unit/integration tests where you assert deep equality.
  • Regression testing with snapshot objects.
  • High-throughput APIs needing object deduplication.
  • Anywhere you need correctness and speed.

📦 Roadmap

  • Strict time semantics
  • Numeric tolerances
  • String comparison options
  • Cycle tracking
  • Include internals & base members
  • Order-insensitive collections
  • Key-based unordered matching
  • Custom comparers
  • Memory<T> / ReadOnlyMemory<T>
  • Benchmarks & tests
  • Analyzer diagnostics
  • Developer guide & samples site

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.3 280 9/15/2025
1.0.2 258 9/15/2025
1.0.1 218 9/14/2025
1.0.0 219 9/14/2025