├── icon.png ├── DeepEqual.Generator.Tests ├── Models │ ├── IAnimal.cs │ ├── Color.cs │ ├── BaseEntity.cs │ ├── Unregistered.cs │ ├── Zoo.cs │ ├── ObjArr.cs │ ├── DoubleWeird.cs │ ├── FloatHolder.cs │ ├── ArrayHolder.cs │ ├── CustomerK.cs │ ├── StringNfcNfd.cs │ ├── ObjList.cs │ ├── Tag.cs │ ├── ChildRef.cs │ ├── GuidHolder.cs │ ├── EnumHolder.cs │ ├── ShallowChild.cs │ ├── StringHolder.cs │ ├── ZooDict.cs │ ├── DateTimeHolder.cs │ ├── ZooArray.cs │ ├── Cat.cs │ ├── NullableStringHolder.cs │ ├── ZooRoList.cs │ ├── EnumerableHolder.cs │ ├── NullableDateTimeHolder.cs │ ├── DateTimeOffsetHolder.cs │ ├── DictionaryHolder.cs │ ├── Item.cs │ ├── MultiDimArrayHolder.cs │ ├── Person.cs │ ├── CycleNode.cs │ ├── Dog.cs │ ├── SimpleStruct.cs │ ├── ZooList.cs │ ├── ContainerWithTypeLevelShallow.cs │ ├── RootOrderSensitiveCollections.cs │ ├── DerivedWithBaseIncluded.cs │ ├── ObjectHolder.cs │ ├── DerivedWithBaseExcluded.cs │ ├── TypeLevelShallowChild.cs │ ├── WithInternalsExcluded.cs │ ├── DateOnlyTimeOnlyHolder.cs │ ├── IntSetHolder.cs │ ├── MemoryHolder.cs │ ├── TagAsElementDefaultUnordered.cs │ ├── CustomersUnkeyed.cs │ ├── DynamicHolder.cs │ ├── RootWithElementTypeDefaultUnordered.cs │ ├── NumericWithComparer.cs │ ├── WithInternalsIncluded.cs │ ├── PersonSetHolder.cs │ ├── CustomComparerHolder.cs │ ├── WithOrderInsensitiveMember.cs │ ├── KeyedBag.cs │ ├── CustomersKeyed.cs │ ├── ZooRoDict.cs │ ├── BadKeyMembersBag.cs │ ├── PolyCycleNode.cs │ ├── OnlySomeMembers.cs │ ├── CompositeKeyBag.cs │ ├── CultureCycleNode.cs │ ├── IgnoreSomeMembers.cs │ ├── CaseInsensitiveStringComparer.cs │ ├── CollectionsOfTimeHolder.cs │ ├── DoubleEpsComparer.cs │ ├── RootOrderInsensitiveCollections.cs │ └── MemberKindContainer.cs ├── Tests │ ├── InvalidAttributesTests.cs │ ├── CultureSensitiveStringTests.cs │ ├── SetTypeTests.cs │ ├── CrazyCycleTests.cs │ ├── StructTests.cs │ ├── ExtraEdgeTests.cs │ ├── RefTypeTests.cs │ └── CollectionsTests.cs └── DeepEqual.Generator.Tests.csproj ├── DeepEqual.Generator.Shared ├── CompareKind.cs ├── IElementComparer.cs ├── ComparisonOptions.cs ├── DeepEqual.Generator.Shared.csproj ├── ComparisonContext.cs ├── DeepComparableAttribute.cs ├── GeneratedHelperRegistry.cs ├── DeepCompareAttribute.cs ├── DynamicDeepComparer.cs └── ComparisonHelpers.cs ├── DeepEqual.Generator.Benchmarking ├── DeepEqual.Generator.Benchmarking.csproj └── Program.cs ├── DeepEqual.Generator └── DeepEqual.Generator.csproj ├── .editorconfig ├── .gitattributes ├── DeepEqual.Generator.sln ├── .gitignore └── Readme.md /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quaverflow/DeepEqualGenerator/HEAD/icon.png -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/IAnimal.cs: -------------------------------------------------------------------------------- 1 | namespace DeepEqual.Generator.Tests.Models; 2 | 3 | public interface IAnimal { int Age { get; } } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/Color.cs: -------------------------------------------------------------------------------- 1 | namespace DeepEqual.Generator.Tests.Models; 2 | 3 | public enum Color 4 | { 5 | Red = 1, 6 | Blue = 2 7 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace DeepEqual.Generator.Tests.Models; 2 | 3 | public class BaseEntity 4 | { 5 | public string? BaseId { get; set; } 6 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/Unregistered.cs: -------------------------------------------------------------------------------- 1 | namespace DeepEqual.Generator.Tests.Models; 2 | 3 | public sealed class Unregistered 4 | { 5 | public int V { get; set; } 6 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/CompareKind.cs: -------------------------------------------------------------------------------- 1 | namespace DeepEqual.Generator.Shared; 2 | 3 | public enum CompareKind 4 | { 5 | Deep, 6 | Shallow, 7 | Reference, 8 | Skip 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/IElementComparer.cs: -------------------------------------------------------------------------------- 1 | namespace DeepEqual.Generator.Shared; 2 | 3 | public interface IElementComparer 4 | { 5 | bool Invoke(T left, T right, ComparisonContext context); 6 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/Zoo.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class Zoo { public IAnimal? Animal { get; init; } } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ObjArr.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class ObjArr { public object? Any { get; init; } } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/DoubleWeird.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class DoubleWeird { public double D { get; init; } } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/FloatHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class FloatHolder { public float F { get; init; } } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ArrayHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class ArrayHolder { public object? Any { get; init; } } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/CustomerK.cs: -------------------------------------------------------------------------------- 1 | namespace DeepEqual.Generator.Tests.Models; 2 | 3 | public sealed class CustomerK 4 | { 5 | public string Id { get; set; } = ""; 6 | public string Name { get; set; } = ""; 7 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/StringNfcNfd.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class StringNfcNfd { public string? S { get; init; } } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ObjList.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class ObjList { public List Items { get; init; } = new(); } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/Tag.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class Tag 7 | { 8 | public string Id { get; set; } = ""; 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ChildRef.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class ChildRef 7 | { 8 | public int Value { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/GuidHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class GuidHolder 7 | { 8 | public Guid Id { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/EnumHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class EnumHolder 7 | { 8 | public Color Shade { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ShallowChild.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class ShallowChild 7 | { 8 | public int V { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/StringHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class StringHolder 7 | { 8 | public string? Value { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ZooDict.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class ZooDict { public Dictionary Pets { get; init; } = new(); } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/DateTimeHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class DateTimeHolder 7 | { 8 | public DateTime When { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ZooArray.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class ZooArray { public IAnimal[] Animals { get; init; } = Array.Empty(); } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/Cat.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class Cat : IAnimal { public int Age { get; init; } public string Name { get; init; } = ""; } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/NullableStringHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class NullableStringHolder 7 | { 8 | public string? Value { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ZooRoList.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class ZooRoList { public IReadOnlyList Animals { get; init; } = new List(); } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/EnumerableHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class EnumerableHolder { public IEnumerable Seq { get; init; } = Array.Empty(); } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/NullableDateTimeHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class NullableDateTimeHolder 7 | { 8 | public DateTime? When { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/DateTimeOffsetHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class DateTimeOffsetHolder 7 | { 8 | public DateTimeOffset When { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/DictionaryHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class DictionaryHolder 7 | { 8 | public Dictionary Map { get; set; } = new(); 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/Item.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class Item 7 | { 8 | public int X { get; set; } 9 | public string? Name { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/MultiDimArrayHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class MultiDimArrayHolder 7 | { 8 | public int[,] Matrix { get; set; } = new int[0, 0]; 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/Person.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class Person 7 | { 8 | public string? Name { get; set; } 9 | public int Age { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/CycleNode.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class CycleNode 7 | { 8 | public int Id { get; set; } 9 | public CycleNode? Next { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/Dog.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class Dog : IAnimal 7 | { 8 | public int Age { get; init; } 9 | public string Name { get; init; } = ""; 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/SimpleStruct.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public struct SimpleStruct 7 | { 8 | public int Id { get; set; } 9 | public DateTime WhenUtc { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ZooList.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models 4 | { 5 | [DeepComparable] 6 | public sealed class ZooList 7 | { 8 | public List Animals { get; init; } = new(); 9 | } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ContainerWithTypeLevelShallow.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class ContainerWithTypeLevelShallow 7 | { 8 | public TypeLevelShallowChild? Child { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/RootOrderSensitiveCollections.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class RootOrderSensitiveCollections 7 | { 8 | public List Names { get; set; } = new(); 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/DerivedWithBaseIncluded.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable(IncludeBaseMembers = true)] 6 | public sealed class DerivedWithBaseIncluded : BaseEntity 7 | { 8 | public string? Name { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ObjectHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class ObjectHolder 7 | { 8 | public object? Any { get; set; } 9 | public ChildRef Known { get; set; } = new(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/DerivedWithBaseExcluded.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable(IncludeBaseMembers = false)] 6 | public sealed class DerivedWithBaseExcluded : BaseEntity 7 | { 8 | public string? Name { get; set; } 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/TypeLevelShallowChild.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepCompare(Kind = CompareKind.Shallow)] 6 | [DeepComparable] 7 | public sealed class TypeLevelShallowChild 8 | { 9 | public int V { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/WithInternalsExcluded.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class WithInternalsExcluded 7 | { 8 | public int Shown { get; set; } 9 | internal int Hidden { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/DateOnlyTimeOnlyHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class DateOnlyTimeOnlyHolder 7 | { 8 | public DateOnly D { get; set; } 9 | public TimeOnly T { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/IntSetHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class IntSetHolder 7 | { 8 | [DeepCompare(OrderInsensitive = true)] 9 | public HashSet Set { get; init; } = new(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/MemoryHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class MemoryHolder 7 | { 8 | public Memory Buf { get; set; } 9 | public ReadOnlyMemory RBuf { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/TagAsElementDefaultUnordered.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable(OrderInsensitiveCollections = true)] 6 | public sealed class TagAsElementDefaultUnordered 7 | { 8 | public string Label { get; set; } = ""; 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/CustomersUnkeyed.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class CustomersUnkeyed 7 | { 8 | [DeepCompare(OrderInsensitive = true)] 9 | public List People { get; set; } = new(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/DynamicHolder.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | using DeepEqual.Generator.Shared; 3 | 4 | namespace DeepEqual.Generator.Tests.Models; 5 | 6 | [DeepComparable] 7 | public sealed class DynamicHolder 8 | { 9 | public IDictionary Data { get; set; } = new ExpandoObject(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/RootWithElementTypeDefaultUnordered.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class RootWithElementTypeDefaultUnordered 7 | { 8 | public List Tags { get; set; } = new(); 9 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/NumericWithComparer.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class NumericWithComparer 7 | { 8 | [DeepCompare(ComparerType = typeof(DoubleEpsComparer))] 9 | public double Value { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/WithInternalsIncluded.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable(IncludeInternals = true)] 6 | public sealed class WithInternalsIncluded 7 | { 8 | public int Shown { get; set; } 9 | internal int Hidden { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/PersonSetHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class PersonSetHolder 7 | { 8 | [DeepCompare(OrderInsensitive = true)] 9 | public ISet People { get; init; } = new HashSet(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/CustomComparerHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class CustomComparerHolder 7 | { 8 | [DeepCompare(ComparerType = typeof(CaseInsensitiveStringComparer))] 9 | public string? Code { get; set; } 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/WithOrderInsensitiveMember.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class WithOrderInsensitiveMember 7 | { 8 | [DeepCompare(OrderInsensitive = true)] 9 | public List Values { get; set; } = new(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/KeyedBag.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class KeyedBag 7 | { 8 | [DeepCompare(OrderInsensitive = true, KeyMembers = new[] { nameof(Item.Name) })] 9 | public List Items { get; init; } = new(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/CustomersKeyed.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class CustomersKeyed 7 | { 8 | [DeepCompare(OrderInsensitive = true, KeyMembers = new[] { "Id" })] 9 | public List Customers { get; set; } = new(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/ZooRoDict.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] public sealed class ZooRoDict { public IReadOnlyDictionary Pets { get; init; } = new System.Collections.ObjectModel.ReadOnlyDictionary(new Dictionary()); } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/BadKeyMembersBag.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class BadKeyMembersBag 7 | { 8 | [DeepCompare(OrderInsensitive = true, KeyMembers = new[] { "Nope", "AlsoNope" })] 9 | public List Items { get; init; } = new(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/PolyCycleNode.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable(CycleTracking = true)] 6 | public sealed class PolyCycleNode 7 | { 8 | public int Id { get; init; } 9 | public IAnimal? Animal { get; init; } 10 | public PolyCycleNode? Next { get; set; } 11 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/OnlySomeMembers.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepCompare(Members = new[] { "A", "B" })] 6 | [DeepComparable] 7 | public sealed class OnlySomeMembers 8 | { 9 | public int A { get; set; } 10 | public int B { get; set; } 11 | public int C { get; set; } 12 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/CompositeKeyBag.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class CompositeKeyBag 7 | { 8 | [DeepCompare(OrderInsensitive = true, KeyMembers = new[] { nameof(Item.Name), nameof(Item.X) })] 9 | public List Items { get; init; } = new(); 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/CultureCycleNode.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using DeepEqual.Generator.Shared; 3 | 4 | namespace DeepEqual.Generator.Tests.Models; 5 | 6 | [DeepComparable(CycleTracking = true)] 7 | public sealed class CultureCycleNode 8 | { 9 | public CultureInfo? Culture { get; init; } 10 | public CultureCycleNode? Next { get; set; } 11 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/IgnoreSomeMembers.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepCompare(IgnoreMembers = new[] { "C" })] 6 | [DeepComparable] 7 | public sealed class IgnoreSomeMembers 8 | { 9 | public int A { get; set; } 10 | public int B { get; set; } 11 | public int C { get; set; } 12 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/CaseInsensitiveStringComparer.cs: -------------------------------------------------------------------------------- 1 | namespace DeepEqual.Generator.Tests.Models; 2 | 3 | public sealed class CaseInsensitiveStringComparer : IEqualityComparer 4 | { 5 | public bool Equals(string? x, string? y) => string.Equals(x, y, StringComparison.OrdinalIgnoreCase); 6 | public int GetHashCode(string obj) => StringComparer.OrdinalIgnoreCase.GetHashCode(obj); 7 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/CollectionsOfTimeHolder.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class CollectionsOfTimeHolder 7 | { 8 | public DateTime[] Snapshots { get; set; } = Array.Empty(); 9 | public List Events { get; set; } = new(); 10 | public Dictionary Index { get; set; } = new(); 11 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/ComparisonOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DeepEqual.Generator.Shared; 4 | 5 | public sealed class ComparisonOptions 6 | { 7 | public StringComparison StringComparison { get; set; } = StringComparison.Ordinal; 8 | public bool TreatNaNEqual { get; set; } = true; 9 | public double DoubleEpsilon { get; set; } = 0.0; 10 | public float FloatEpsilon { get; set; } = 0f; 11 | public decimal DecimalEpsilon { get; set; } = 0m; 12 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/DoubleEpsComparer.cs: -------------------------------------------------------------------------------- 1 | namespace DeepEqual.Generator.Tests.Models; 2 | 3 | public sealed class DoubleEpsComparer : IEqualityComparer 4 | { 5 | private readonly double _eps; 6 | public DoubleEpsComparer() : this(1e-6) { } 7 | public DoubleEpsComparer(double eps) { _eps = eps; } 8 | public bool Equals(double x, double y) => Math.Abs(x - y) <= _eps || double.IsNaN(x) && double.IsNaN(y); 9 | public int GetHashCode(double obj) => 0; 10 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/RootOrderInsensitiveCollections.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable(OrderInsensitiveCollections = true)] 6 | public sealed class RootOrderInsensitiveCollections 7 | { 8 | public List Names { get; set; } = new(); 9 | public List People { get; set; } = new(); 10 | [DeepCompare(OrderInsensitive = false)] 11 | public List ForcedOrdered { get; set; } = new(); 12 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Models/MemberKindContainer.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | 3 | namespace DeepEqual.Generator.Tests.Models; 4 | 5 | [DeepComparable] 6 | public sealed class MemberKindContainer 7 | { 8 | public Item ValDeep { get; set; } = new(); 9 | [DeepCompare(Kind = CompareKind.Shallow)] 10 | public Item ValShallow { get; set; } = new(); 11 | [DeepCompare(Kind = CompareKind.Reference)] 12 | public Item ValReference { get; set; } = new(); 13 | [DeepCompare(Kind = CompareKind.Skip)] 14 | public Item ValSkipped { get; set; } = new(); 15 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Tests/InvalidAttributesTests.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Tests.Models; 2 | 3 | namespace DeepEqual.Generator.Tests.Tests; 4 | 5 | public class InvalidAttributesTests 6 | { 7 | [Fact] 8 | public void Bad_KeyMembers_Falls_Back_To_Unkeyed_Unordered() 9 | { 10 | var a = new BadKeyMembersBag { Items = new() { new Item { Name = "a", X = 1 }, new Item { Name = "b", X = 2 } } }; 11 | var b = new BadKeyMembersBag { Items = new() { new Item { Name = "b", X = 2 }, new Item { Name = "a", X = 1 } } }; 12 | var c = new BadKeyMembersBag { Items = new() { new Item { Name = "a", X = 1 } } }; 13 | 14 | Assert.True(BadKeyMembersBagDeepEqual.AreDeepEqual(a, b)); 15 | Assert.False(BadKeyMembersBagDeepEqual.AreDeepEqual(a, c)); 16 | } 17 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/DeepEqual.Generator.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Tests/CultureSensitiveStringTests.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using DeepEqual.Generator.Shared; 3 | using DeepEqual.Generator.Tests.Models; 4 | 5 | namespace DeepEqual.Generator.Tests.Tests; 6 | 7 | public class CultureSensitiveStringTests 8 | { 9 | [Fact] 10 | public void Turkish_Case_Insensitive_Compare_Uses_CurrentCulture() 11 | { 12 | var prev = CultureInfo.CurrentCulture; 13 | try 14 | { 15 | CultureInfo.CurrentCulture = new CultureInfo("tr-TR"); 16 | 17 | var a = new StringHolder { Value = "I" }; var b = new StringHolder { Value = "ı" }; 18 | var opts = new ComparisonOptions { StringComparison = StringComparison.CurrentCultureIgnoreCase }; 19 | Assert.True(StringHolderDeepEqual.AreDeepEqual(a, b, opts)); 20 | 21 | var ordinal = new ComparisonOptions { StringComparison = StringComparison.OrdinalIgnoreCase }; 22 | Assert.False(StringHolderDeepEqual.AreDeepEqual(a, b, ordinal)); 23 | } 24 | finally 25 | { 26 | CultureInfo.CurrentCulture = prev; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/DeepEqual.Generator.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | true 5 | DeepEqual.Generator.Shared 6 | 1.0.1 7 | Runtime comparers and attributes for DeepEqual generator. 8 | https://github.com/Quaverflow/DeepEqualGenerator 9 | MIT 10 | Readme.md 11 | icon.png 12 | true 13 | true 14 | snupkg 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DeepEqual.Generator.Benchmarking/DeepEqual.Generator.Benchmarking.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Tests/SetTypeTests.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Tests.Models; 2 | 3 | namespace DeepEqual.Generator.Tests.Tests; 4 | 5 | public class SetTypeTests 6 | { 7 | [Fact] 8 | public void HashSet_Int_Order_Irrelevant_Content_Must_Match() 9 | { 10 | var a = new IntSetHolder { Set = new HashSet { 1, 2, 3 } }; 11 | var b = new IntSetHolder { Set = new HashSet { 3, 2, 1 } }; 12 | var c = new IntSetHolder { Set = new HashSet { 1, 2 } }; 13 | 14 | Assert.True(IntSetHolderDeepEqual.AreDeepEqual(a, b)); 15 | Assert.False(IntSetHolderDeepEqual.AreDeepEqual(a, c)); 16 | } 17 | 18 | [Fact] 19 | public void ISet_Of_Pocos_Deep_Element_Compare() 20 | { 21 | var a = new PersonSetHolder { People = new HashSet { new() { Name = "a", Age = 1 }, new() { Name = "b", Age = 2 } } }; 22 | var b = new PersonSetHolder { People = new HashSet { new() { Name = "b", Age = 2 }, new() { Name = "a", Age = 1 } } }; 23 | var c = new PersonSetHolder { People = new HashSet { new() { Name = "a", Age = 1 }, new() { Name = "b", Age = 99 } } }; 24 | 25 | Assert.True(PersonSetHolderDeepEqual.AreDeepEqual(a, b)); 26 | Assert.False(PersonSetHolderDeepEqual.AreDeepEqual(a, c)); 27 | } 28 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Tests/CrazyCycleTests.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using DeepEqual.Generator.Tests.Models; 3 | 4 | namespace DeepEqual.Generator.Tests.Tests; 5 | 6 | public class CrazyCycleTests 7 | { 8 | [Fact] 9 | public void Heterogeneous_IAnimal_Cycle_Equal_And_NotEqual() 10 | { 11 | var a1 = new PolyCycleNode { Id = 1, Animal = new Cat { Age = 2, Name = "C" } }; 12 | var a2 = new PolyCycleNode { Id = 2, Animal = new Dog { Age = 5, Name = "D" } }; 13 | a1.Next = a2; a2.Next = a1; 14 | 15 | var b1 = new PolyCycleNode { Id = 1, Animal = new Cat { Age = 2, Name = "C" } }; 16 | var b2 = new PolyCycleNode { Id = 2, Animal = new Dog { Age = 5, Name = "D" } }; 17 | b1.Next = b2; b2.Next = b1; 18 | 19 | Assert.True(PolyCycleNodeDeepEqual.AreDeepEqual(a1, b1)); 20 | 21 | var b2diff = new PolyCycleNode { Id = 2, Animal = new Dog { Age = 6, Name = "D" } }; 22 | b1.Next = b2diff; b2diff.Next = b1; 23 | Assert.False(PolyCycleNodeDeepEqual.AreDeepEqual(a1, b1)); 24 | 25 | var b2Type = new PolyCycleNode { Id = 2, Animal = new Cat { Age = 5, Name = "D" } }; 26 | b1.Next = b2Type; b2Type.Next = b1; 27 | Assert.False(PolyCycleNodeDeepEqual.AreDeepEqual(a1, b1)); 28 | } 29 | 30 | [Fact] 31 | public void CultureInfo_In_Cycle_Compares_By_Instance_Semantics() 32 | { 33 | var enGb1 = CultureInfo.GetCultureInfo("en-GB"); 34 | var enGb2 = CultureInfo.GetCultureInfo("en-GB"); 35 | var itIt = CultureInfo.GetCultureInfo("it-IT"); 36 | 37 | var a = new CultureCycleNode { Culture = enGb1 }; 38 | a.Next = a; 39 | 40 | var b = new CultureCycleNode { Culture = enGb2 }; 41 | b.Next = b; 42 | 43 | var c = new CultureCycleNode { Culture = itIt }; 44 | c.Next = c; 45 | 46 | Assert.True(CultureCycleNodeDeepEqual.AreDeepEqual(a, b)); 47 | Assert.False(CultureCycleNodeDeepEqual.AreDeepEqual(a, c)); 48 | } 49 | } -------------------------------------------------------------------------------- /DeepEqual.Generator/DeepEqual.Generator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | latest 5 | enable 6 | true 7 | true 8 | false 9 | true 10 | DeepEqual.Generator 11 | 1.0.3 12 | Mirko Sangrigoli 13 | Quaverflow 14 | High-performance deep equality source generator for .NET. 15 | deep-equality;source-generator;dotnet;performance;analyzers 16 | https://github.com/Quaverflow/DeepEqualGenerator 17 | MIT 18 | Readme.md 19 | icon.png 20 | true 21 | true 22 | snupkg 23 | true 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # IDE0022: Use block body for method 4 | csharp_style_expression_bodied_methods = false 5 | csharp_indent_labels = one_less_than_current 6 | 7 | [*.{cs,vb}] 8 | #### Naming styles #### 9 | 10 | # Naming rules 11 | 12 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 13 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 14 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 15 | 16 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 17 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 18 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 19 | 20 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 21 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 22 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 23 | 24 | # Symbol specifications 25 | 26 | dotnet_naming_symbols.interface.applicable_kinds = interface 27 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 28 | dotnet_naming_symbols.interface.required_modifiers = 29 | 30 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 31 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 32 | dotnet_naming_symbols.types.required_modifiers = 33 | 34 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 35 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 36 | dotnet_naming_symbols.non_field_members.required_modifiers = 37 | 38 | # Naming styles 39 | 40 | dotnet_naming_style.begins_with_i.required_prefix = I 41 | dotnet_naming_style.begins_with_i.required_suffix = 42 | dotnet_naming_style.begins_with_i.word_separator = 43 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 44 | 45 | dotnet_naming_style.pascal_case.required_prefix = 46 | dotnet_naming_style.pascal_case.required_suffix = 47 | dotnet_naming_style.pascal_case.word_separator = 48 | dotnet_naming_style.pascal_case.capitalization = pascal_case 49 | 50 | dotnet_naming_style.pascal_case.required_prefix = 51 | dotnet_naming_style.pascal_case.required_suffix = 52 | dotnet_naming_style.pascal_case.word_separator = 53 | dotnet_naming_style.pascal_case.capitalization = pascal_case 54 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 55 | tab_width = 4 56 | indent_size = 4 57 | end_of_line = crlf 58 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/ComparisonContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace DeepEqual.Generator.Shared; 5 | 6 | public sealed class ComparisonContext 7 | { 8 | private readonly bool _tracking; 9 | private readonly HashSet _visited; 10 | private readonly Stack _stack; 11 | 12 | public ComparisonOptions Options { get; } 13 | 14 | public static ComparisonContext NoTracking { get; } = new(false, new ComparisonOptions()); 15 | 16 | public ComparisonContext() : this(true, new ComparisonOptions()) { } 17 | 18 | public ComparisonContext(ComparisonOptions? options) : this(true, options ?? new ComparisonOptions()) { } 19 | 20 | private ComparisonContext(bool enableTracking, ComparisonOptions? options) 21 | { 22 | _tracking = enableTracking; 23 | Options = options ?? new ComparisonOptions(); 24 | if (_tracking) 25 | { 26 | _visited = new HashSet(RefPair.Comparer.Instance); 27 | _stack = new Stack(); 28 | } 29 | else 30 | { 31 | _visited = null!; 32 | _stack = null!; 33 | } 34 | } 35 | 36 | public bool Enter(object left, object right) 37 | { 38 | if (!_tracking) 39 | { 40 | return true; 41 | } 42 | 43 | var pair = new RefPair(left, right); 44 | if (!_visited.Add(pair)) 45 | { 46 | return false; 47 | } 48 | 49 | _stack.Push(pair); 50 | return true; 51 | } 52 | 53 | public void Exit(object left, object right) 54 | { 55 | if (!_tracking) 56 | { 57 | return; 58 | } 59 | 60 | if (_stack.Count == 0) 61 | { 62 | return; 63 | } 64 | 65 | var last = _stack.Pop(); 66 | _visited.Remove(last); 67 | } 68 | 69 | private readonly struct RefPair(object left, object right) 70 | { 71 | private readonly object _left = left; 72 | private readonly object _right = right; 73 | 74 | public sealed class Comparer : IEqualityComparer 75 | { 76 | public static readonly Comparer Instance = new(); 77 | public bool Equals(RefPair x, RefPair y) => ReferenceEquals(x._left, y._left) && ReferenceEquals(x._right, y._right); 78 | public int GetHashCode(RefPair obj) 79 | { 80 | unchecked 81 | { 82 | var a = RuntimeHelpers.GetHashCode(obj._left); 83 | var b = RuntimeHelpers.GetHashCode(obj._right); 84 | return (a * 397) ^ b; 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/DeepComparableAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DeepEqual.Generator.Shared 4 | { 5 | /// 6 | /// Marks a class or struct as a root for generated deep comparison helpers and sets defaults for that type. 7 | /// 8 | /// 9 | /// 10 | /// Apply this to any model you want to compare deeply. A static helper named 11 | /// {TypeName}DeepEqual will be generated with AreDeepEqual(left, right). 12 | /// 13 | /// 14 | /// Nested types and referenced user types are included automatically when they appear under the root. 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// [DeepComparable] 20 | /// public sealed class Order 21 | /// { 22 | /// public string Id { get; set; } = ""; 23 | /// public List<OrderLine> Lines { get; set; } = new(); 24 | /// } 25 | /// 26 | /// // Usage: 27 | /// var equal = OrderDeepEqual.AreDeepEqual(orderA, orderB); 28 | /// 29 | /// 30 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] 31 | public sealed class DeepComparableAttribute : Attribute 32 | { 33 | /// 34 | /// Treat all collections under this type as unordered by default. 35 | /// 36 | /// 37 | /// 38 | /// When , list and array order does not matter unless a member says otherwise. 39 | /// Duplicates still count (multiset behavior). 40 | /// 41 | /// 42 | /// Member-level settings with take priority. 43 | /// 44 | /// 45 | public bool OrderInsensitiveCollections { get; set; } 46 | 47 | /// 48 | /// Enable cycle tracking for this type's object graphs. 49 | /// 50 | /// 51 | /// 52 | /// When , the comparer remembers pairs it has already visited so it can safely 53 | /// handle graphs with loops (e.g., parent <-> child). This prevents infinite recursion. 54 | /// 55 | /// 56 | /// Leave this off only if you are sure your graphs have no cycles, and you want the absolute minimum overhead. 57 | /// 58 | /// 59 | public bool CycleTracking { get; set; } 60 | 61 | /// 62 | /// Include internal members and compare internal types from the same assembly. 63 | /// 64 | /// 65 | /// 66 | /// This does not include private or protected members. 67 | /// 68 | /// 69 | public bool IncludeInternals { get; set; } 70 | 71 | /// 72 | /// Include members from base classes by default. 73 | /// 74 | /// 75 | /// 76 | /// If , members declared on base classes are part of the comparison. 77 | /// You can turn this off on a specific type by setting it to . 78 | /// 79 | /// 80 | public bool IncludeBaseMembers { get; set; } = true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/GeneratedHelperRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace DeepEqual.Generator.Shared; 6 | 7 | public static class GeneratedHelperRegistry 8 | { 9 | private static readonly ConcurrentDictionary> ComparerMap = new(); 10 | private static readonly ConcurrentDictionary NegativeCache = new(); 11 | 12 | public static void Register(Func comparer) 13 | { 14 | var t = typeof(T); 15 | ComparerMap[t] = (l, r, c) => comparer((T)l, (T)r, c); 16 | NegativeCache.TryRemove(t, out _); } 17 | 18 | public static bool TryCompare(object left, object right, ComparisonContext context, out bool equal) 19 | { 20 | if (ReferenceEquals(left, right)) { equal = true; return true; } 21 | if (left is null || right is null) { equal = false; return true; } 22 | var t = left.GetType(); 23 | if (t != right.GetType()) { equal = false; return true; } 24 | return TryCompareSameType(t, left, right, context, out equal); 25 | } 26 | 27 | public static bool TryCompareSameType(Type runtimeType, object left, object right, ComparisonContext context, out bool equal) 28 | { 29 | if (ComparerMap.TryGetValue(runtimeType, out var comparer)) 30 | { 31 | equal = comparer(left, right, context); 32 | return true; 33 | } 34 | if (TryCompareAssignable(runtimeType, left, right, context, out equal)) 35 | { 36 | return true; 37 | } 38 | if (NegativeCache.TryGetValue(runtimeType, out var neg) && neg) 39 | { 40 | equal = false; 41 | return false; 42 | } 43 | 44 | NegativeCache[runtimeType] = true; 45 | equal = false; 46 | return false; 47 | } 48 | 49 | public static void WarmUp(Type runtimeType) 50 | { 51 | var asm = runtimeType.Assembly; 52 | var ns = runtimeType.Namespace; 53 | var name = runtimeType.Name; 54 | var backtick = name.IndexOf('`'); 55 | if (backtick >= 0) 56 | { 57 | name = name[..backtick]; 58 | } 59 | 60 | var helperFullName = (string.IsNullOrEmpty(ns) ? "" : ns + ".") + name + "DeepEqual"; 61 | var helper = asm.GetType(helperFullName, throwOnError: false); 62 | if (helper != null) 63 | { 64 | RuntimeHelpers.RunClassConstructor(helper.TypeHandle); 65 | NegativeCache.TryRemove(runtimeType, out _); 66 | } 67 | } 68 | 69 | private static bool TryCompareAssignable(Type runtimeType, object left, object right, ComparisonContext context, out bool equal) 70 | { 71 | for (var t = runtimeType.BaseType; t != null; t = t.BaseType) 72 | { 73 | if (ComparerMap.TryGetValue(t, out var cmp)) 74 | { 75 | equal = cmp(left, right, context); 76 | return true; 77 | } 78 | } 79 | 80 | foreach (var i in runtimeType.GetInterfaces()) 81 | { 82 | if (ComparerMap.TryGetValue(i, out var cmp)) 83 | { 84 | equal = cmp(left, right, context); 85 | return true; 86 | } 87 | } 88 | 89 | equal = false; 90 | return false; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DeepEqual.Generator.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.14.36310.24 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepEqual.Generator", "DeepEqual.Generator\DeepEqual.Generator.csproj", "{4726E40C-B144-4BEA-AA1D-E65E73C57671}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | Readme.md = Readme.md 12 | EndProjectSection 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5FB800ED-6954-4F9D-9DD5-15FDB9A54847}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepEqual.Generator.Benchmarking", "DeepEqual.Generator.Benchmarking\DeepEqual.Generator.Benchmarking.csproj", "{5AAAB852-9AA1-4AD4-B9BB-6EAA5A65C612}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepEqual.Generator.Shared", "DeepEqual.Generator.Shared\DeepEqual.Generator.Shared.csproj", "{E9458AAC-186F-EBE7-33FF-B19E5EA1E476}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepEqual.Generator.Tests", "DeepEqual.Generator.Tests\DeepEqual.Generator.Tests.csproj", "{B9A064FC-381D-1C45-F37A-0B8CDB4FD0E3}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {4726E40C-B144-4BEA-AA1D-E65E73C57671}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {4726E40C-B144-4BEA-AA1D-E65E73C57671}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {4726E40C-B144-4BEA-AA1D-E65E73C57671}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {4726E40C-B144-4BEA-AA1D-E65E73C57671}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {5AAAB852-9AA1-4AD4-B9BB-6EAA5A65C612}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {5AAAB852-9AA1-4AD4-B9BB-6EAA5A65C612}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {5AAAB852-9AA1-4AD4-B9BB-6EAA5A65C612}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {5AAAB852-9AA1-4AD4-B9BB-6EAA5A65C612}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {E9458AAC-186F-EBE7-33FF-B19E5EA1E476}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {E9458AAC-186F-EBE7-33FF-B19E5EA1E476}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {E9458AAC-186F-EBE7-33FF-B19E5EA1E476}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {E9458AAC-186F-EBE7-33FF-B19E5EA1E476}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {B9A064FC-381D-1C45-F37A-0B8CDB4FD0E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {B9A064FC-381D-1C45-F37A-0B8CDB4FD0E3}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {B9A064FC-381D-1C45-F37A-0B8CDB4FD0E3}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {B9A064FC-381D-1C45-F37A-0B8CDB4FD0E3}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {5AAAB852-9AA1-4AD4-B9BB-6EAA5A65C612} = {5FB800ED-6954-4F9D-9DD5-15FDB9A54847} 50 | {B9A064FC-381D-1C45-F37A-0B8CDB4FD0E3} = {5FB800ED-6954-4F9D-9DD5-15FDB9A54847} 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {F7EFDDC0-018A-409C-9C08-12C99B8F2C79} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Tests/StructTests.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Tests.Models; 2 | 3 | namespace DeepEqual.Generator.Tests.Tests; 4 | 5 | public class StructTests 6 | { 7 | [Fact] 8 | public void Structs_With_Different_DateTimeKind_Fail() 9 | { 10 | var a = new SimpleStruct { WhenUtc = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc) }; 11 | var b = new SimpleStruct { WhenUtc = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Local) }; 12 | Assert.False(SimpleStructDeepEqual.AreDeepEqual(a, b)); 13 | } 14 | 15 | [Fact] 16 | public void DateTime_Strict_Cases() 17 | { 18 | var tU1 = new DateTime(2025, 1, 1, 12, 0, 0, DateTimeKind.Utc); 19 | var tU2 = new DateTime(2025, 1, 1, 12, 0, 0, DateTimeKind.Utc); 20 | Assert.True(DateTimeHolderDeepEqual.AreDeepEqual(new DateTimeHolder { When = tU1 }, new DateTimeHolder { When = tU2 })); 21 | 22 | var tKindDiff = new DateTime(2025, 1, 1, 12, 0, 0, DateTimeKind.Local); 23 | Assert.False(DateTimeHolderDeepEqual.AreDeepEqual(new DateTimeHolder { When = tU1 }, new DateTimeHolder { When = tKindDiff })); 24 | 25 | var tTickDiff = new DateTime(2025, 1, 1, 12, 0, 1, DateTimeKind.Utc); 26 | Assert.False(DateTimeHolderDeepEqual.AreDeepEqual(new DateTimeHolder { When = tU1 }, new DateTimeHolder { When = tTickDiff })); 27 | } 28 | 29 | [Fact] 30 | public void DateTimeOffset_Strict_Cases() 31 | { 32 | var dt1 = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.FromHours(1)); 33 | var dt2 = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.FromHours(1)); 34 | Assert.True(DateTimeOffsetHolderDeepEqual.AreDeepEqual(new DateTimeOffsetHolder { When = dt1 }, new DateTimeOffsetHolder { When = dt2 })); 35 | 36 | var dtoOff = dt1.ToOffset(TimeSpan.FromHours(2)); 37 | Assert.False(DateTimeOffsetHolderDeepEqual.AreDeepEqual(new DateTimeOffsetHolder { When = dt1 }, new DateTimeOffsetHolder { When = dtoOff })); 38 | } 39 | 40 | [Fact] 41 | public void Nullable_DateTime_Cases() 42 | { 43 | Assert.True(NullableDateTimeHolderDeepEqual.AreDeepEqual(new NullableDateTimeHolder { When = null }, new NullableDateTimeHolder { When = null })); 44 | Assert.False(NullableDateTimeHolderDeepEqual.AreDeepEqual(new NullableDateTimeHolder { When = null }, new NullableDateTimeHolder { When = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc) })); 45 | Assert.False(NullableDateTimeHolderDeepEqual.AreDeepEqual(new NullableDateTimeHolder { When = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc) }, new NullableDateTimeHolder { When = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Local) })); 46 | } 47 | 48 | [Fact] 49 | public void Enum_Equality_By_Value() 50 | { 51 | var a = new EnumHolder { Shade = Color.Blue }; 52 | var b = new EnumHolder { Shade = Color.Blue }; 53 | var c = new EnumHolder { Shade = Color.Red }; 54 | Assert.True(EnumHolderDeepEqual.AreDeepEqual(a, b)); 55 | Assert.False(EnumHolderDeepEqual.AreDeepEqual(a, c)); 56 | } 57 | 58 | [Fact] 59 | public void Guid_Equality_By_Value() 60 | { 61 | var g = Guid.NewGuid(); 62 | var a = new GuidHolder { Id = g }; 63 | var b = new GuidHolder { Id = g }; 64 | var c = new GuidHolder { Id = Guid.NewGuid() }; 65 | Assert.True(GuidHolderDeepEqual.AreDeepEqual(a, b)); 66 | Assert.False(GuidHolderDeepEqual.AreDeepEqual(a, c)); 67 | } 68 | 69 | [Fact] 70 | public void DateOnly_TimeOnly_Strict() 71 | { 72 | var a = new DateOnlyTimeOnlyHolder { D = new DateOnly(2025, 1, 1), T = new TimeOnly(12, 0, 0) }; 73 | var b = new DateOnlyTimeOnlyHolder { D = new DateOnly(2025, 1, 1), T = new TimeOnly(12, 0, 0) }; 74 | var c = new DateOnlyTimeOnlyHolder { D = new DateOnly(2025, 1, 2), T = new TimeOnly(12, 0, 0) }; 75 | var d = new DateOnlyTimeOnlyHolder { D = new DateOnly(2025, 1, 1), T = new TimeOnly(12, 0, 1) }; 76 | Assert.True(DateOnlyTimeOnlyHolderDeepEqual.AreDeepEqual(a, b)); 77 | Assert.False(DateOnlyTimeOnlyHolderDeepEqual.AreDeepEqual(a, c)); 78 | Assert.False(DateOnlyTimeOnlyHolderDeepEqual.AreDeepEqual(a, d)); 79 | } 80 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Tests/ExtraEdgeTests.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Generator.Shared; 2 | using DeepEqual.Generator.Tests.Models; 3 | 4 | namespace DeepEqual.Generator.Tests.Tests; 5 | 6 | public class ExtraEdgeTests 7 | { 8 | [Fact] 9 | public void Double_NaN_And_SignedZero_Options() 10 | { 11 | var a = new DoubleWeird { D = double.NaN }; 12 | var b = new DoubleWeird { D = double.NaN }; 13 | var allow = new ComparisonOptions { TreatNaNEqual = true }; 14 | var deny = new ComparisonOptions { TreatNaNEqual = false }; 15 | Assert.True(DoubleWeirdDeepEqual.AreDeepEqual(a, b, allow)); 16 | Assert.False(DoubleWeirdDeepEqual.AreDeepEqual(a, b, deny)); 17 | 18 | var z1 = new DoubleWeird { D = +0.0 }; 19 | var z2 = new DoubleWeird { D = -0.0 }; 20 | var strict = new ComparisonOptions { DoubleEpsilon = 0.0 }; 21 | Assert.True(DoubleWeirdDeepEqual.AreDeepEqual(z1, z2, strict)); 22 | } 23 | 24 | [Fact] 25 | public void Float_Uses_Single_Epsilon() 26 | { 27 | var a = new FloatHolder { F = 1.000001f }; 28 | var b = new FloatHolder { F = 1.000002f }; 29 | var loose = new ComparisonOptions { FloatEpsilon = 0.00001f }; 30 | var strict = new ComparisonOptions { FloatEpsilon = 0f }; 31 | Assert.True(FloatHolderDeepEqual.AreDeepEqual(a, b, loose)); 32 | Assert.False(FloatHolderDeepEqual.AreDeepEqual(a, b, strict)); 33 | } 34 | 35 | [Fact] 36 | public void String_Does_Not_Normalize_Combining_Marks() 37 | { 38 | var nfc = new StringNfcNfd { S = "é" }; 39 | var nfd = new StringNfcNfd { S = "e\u0301" }; 40 | var opts = new ComparisonOptions { StringComparison = StringComparison.Ordinal }; 41 | Assert.False(StringNfcNfdDeepEqual.AreDeepEqual(nfc, nfd, opts)); 42 | } 43 | 44 | [Fact] 45 | public void Collections_With_Nulls_Are_Handled() 46 | { 47 | var a = new ObjList { Items = new() { 1, null, new[] { "x" } } }; 48 | var b = new ObjList { Items = new() { 1, null, new[] { "x" } } }; 49 | var c = new ObjList { Items = new() { 1, null, new[] { "y" } } }; 50 | Assert.True(ObjListDeepEqual.AreDeepEqual(a, b)); 51 | Assert.False(ObjListDeepEqual.AreDeepEqual(a, c)); 52 | } 53 | 54 | [Fact] 55 | public void Polymorphism_Inside_Collections() 56 | { 57 | var a = new ZooList { Animals = new() { new Cat { Age = 2, Name = "Paws" }, new Cat { Age = 5, Name = "Mews" } } }; 58 | var b = new ZooList { Animals = new() { new Cat { Age = 2, Name = "Paws" }, new Cat { Age = 5, Name = "Mews" } } }; 59 | var c = new ZooList { Animals = new() { new Cat { Age = 2, Name = "Paws" }, new Cat { Age = 5, Name = "Mewz" } } }; 60 | Assert.True(ZooListDeepEqual.AreDeepEqual(a, b)); 61 | Assert.False(ZooListDeepEqual.AreDeepEqual(a, c)); 62 | } 63 | 64 | [DeepComparable] public sealed class BucketItem { public string K { get; init; } = ""; public int V { get; init; } } 65 | [DeepComparable] 66 | public sealed class Bucketed 67 | { 68 | [DeepCompare(OrderInsensitive = true, KeyMembers = new[] { nameof(BucketItem.K) })] 69 | public List Items { get; init; } = new(); 70 | } 71 | 72 | [Fact] 73 | public void Keyed_Unordered_SameCounts_But_DeepValue_Diff_Is_False() 74 | { 75 | var a = new Bucketed { Items = new() { new() { K = "a", V = 1 }, new() { K = "a", V = 2 } } }; 76 | var b = new Bucketed { Items = new() { new() { K = "a", V = 2 }, new() { K = "a", V = 1 } } }; 77 | var c = new Bucketed { Items = new() { new() { K = "a", V = 1 }, new() { K = "a", V = 99 } } }; 78 | Assert.True(BucketedDeepEqual.AreDeepEqual(a, b)); 79 | Assert.False(BucketedDeepEqual.AreDeepEqual(a, c)); 80 | } 81 | 82 | [DeepComparable] public sealed class DictShapeA { public Dictionary Map { get; init; } = new(); } 83 | public sealed class CustomDict : Dictionary { } [DeepComparable] public sealed class DictShapeB { public CustomDict Map { get; init; } = new(); } 84 | 85 | [Fact] 86 | public void Dictionary_Fallback_Mixed_Shapes_Work() 87 | { 88 | var a = new DictShapeA { Map = new() { ["x"] = 1, ["y"] = 2 } }; 89 | var b = new DictShapeB { Map = new CustomDict { ["y"] = 2, ["x"] = 1 } }; 90 | Assert.True(DictShapeADeepEqual.AreDeepEqual(a, new DictShapeA { Map = new() { ["x"] = 1, ["y"] = 2 } })); 91 | Assert.True(DictShapeBDeepEqual.AreDeepEqual(b, new DictShapeB { Map = new CustomDict { ["x"] = 1, ["y"] = 2 } })); 92 | } 93 | 94 | [Fact] 95 | public void Symmetry_And_Repeatability() 96 | { 97 | var a = new ObjList { Items = new() { "a", "b" } }; 98 | var b = new ObjList { Items = new() { "a", "b" } }; 99 | Assert.True(ObjListDeepEqual.AreDeepEqual(a, b)); 100 | Assert.True(ObjListDeepEqual.AreDeepEqual(b, a)); 101 | Assert.True(ObjListDeepEqual.AreDeepEqual(a, b)); } 102 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/DeepCompareAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DeepEqual.Generator.Shared; 4 | 5 | /// 6 | /// Overrides how a member (property/field) or a whole type is compared. 7 | /// 8 | /// 9 | /// Apply this to: 10 | /// 11 | /// a property or field — to control how that single member is compared; 12 | /// a class or struct — to set the default comparison rules wherever that type appears. 13 | /// 14 | /// Member-level settings take priority over type-level settings. 15 | /// 16 | /// 17 | /// 18 | /// public sealed class Order 19 | /// { 20 | /// // Only check reference (same object) for this member 21 | /// [DeepCompare(Kind = CompareKind.Reference)] 22 | /// public byte[]? RawPayload { get; set; } 23 | /// 24 | /// // Ignore order for this list (treat like a bag) 25 | /// [DeepCompare(OrderInsensitive = true)] 26 | /// public List<string> Tags { get; set; } = new(); 27 | /// } 28 | /// 29 | /// 30 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct)] 31 | public sealed class DeepCompareAttribute : Attribute 32 | { 33 | /// 34 | /// How to compare the target. 35 | /// 36 | /// 37 | /// Default is (walks into nested members). 38 | /// Use to call .Equals only, 39 | /// to require the same object instance, 40 | /// or to ignore the member. 41 | /// 42 | public CompareKind Kind { get; set; } = CompareKind.Deep; 43 | 44 | /// 45 | /// When the target is a sequence (array or collection), compare as unordered. 46 | /// 47 | /// 48 | /// 49 | /// If , element order does not matter (duplicates still must match). 50 | /// If , elements are compared in order (index by index). 51 | /// 52 | /// 53 | /// This setting affects only the annotated member or type. It does not change others. 54 | /// 55 | /// 56 | public bool OrderInsensitive { get; set; } = false; 57 | 58 | /// 59 | /// Only compare these member names of the annotated type. 60 | /// 61 | /// 62 | /// 63 | /// Use on a type to narrow which members participate in equality. 64 | /// If set, any member not listed here is ignored. 65 | /// 66 | /// 67 | /// Member names must match exactly (including case). 68 | /// 69 | /// 70 | public string[] Members { get; set; } = []; 71 | 72 | /// 73 | /// Ignore these member names during comparison. 74 | /// 75 | /// 76 | /// 77 | /// Useful for skipping audit fields (e.g., UpdatedAt) or transient caches. 78 | /// 79 | /// 80 | /// If a name appears in both and , 81 | /// it is ignored. 82 | /// 83 | /// 84 | public string[] IgnoreMembers { get; set; } = []; 85 | 86 | /// 87 | /// Custom equality comparer type to use for this member or type. 88 | /// 89 | /// 90 | /// 91 | /// The type must implement IEqualityComparer<T> where T matches the 92 | /// member's (or type's) element type. The generator will create and use an instance of it. 93 | /// 94 | /// 95 | /// Example: a case-insensitive string comparer for a string property. 96 | /// 97 | /// 98 | /// 99 | /// 100 | /// public sealed class CaseInsensitive : IEqualityComparer<string> 101 | /// { 102 | /// public bool Equals(string? a, string? b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase); 103 | /// public int GetHashCode(string s) => StringComparer.OrdinalIgnoreCase.GetHashCode(s); 104 | /// } 105 | /// 106 | /// public sealed class Product 107 | /// { 108 | /// [DeepCompare(ComparerType = typeof(CaseInsensitive))] 109 | /// public string? Sku { get; set; } 110 | /// } 111 | /// 112 | /// 113 | public Type? ComparerType { get; set; } = null; 114 | 115 | /// 116 | /// Property/field names that act as keys when matching items in an unordered collection of objects. 117 | /// 118 | /// 119 | /// 120 | /// Use this when comparing two lists of objects where order does not matter, but each item has an identity. 121 | /// Items with the same key are paired and then compared deeply. 122 | /// 123 | /// 124 | /// For a single key, specify one name (e.g., "Id"). 125 | /// For a composite key, list multiple names (e.g., {"Id","Region"}). 126 | /// 127 | /// 128 | /// Keys must exist on the element type and be comparable (strings, numbers, etc.). 129 | /// 130 | /// 131 | /// 132 | /// 133 | /// public sealed class Customer { public string Id { get; set; } = ""; public string Name { get; set; } = ""; } 134 | /// 135 | /// public sealed class Batch 136 | /// { 137 | /// [DeepCompare(OrderInsensitive = true, KeyMembers = new[] { "Id" })] 138 | /// public List<Customer> Customers { get; set; } = new(); 139 | /// } 140 | /// 141 | /// 142 | public string[] KeyMembers { get; set; } = []; 143 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/DynamicDeepComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace DeepEqual.Generator.Shared; 6 | 7 | public static class DynamicDeepComparer 8 | { 9 | public static bool AreEqualDynamic(object? left, object? right, ComparisonContext context) 10 | { 11 | if (ReferenceEquals(left, right)) 12 | { 13 | return true; 14 | } 15 | 16 | if (left is null || right is null) 17 | { 18 | return false; 19 | } 20 | 21 | if (left is string sa && right is string sb) 22 | { 23 | return ComparisonHelpers.AreEqualStrings(sa, sb, context); 24 | } 25 | 26 | if (left is double da && right is double db) 27 | { 28 | return ComparisonHelpers.AreEqualDouble(da, db, context); 29 | } 30 | 31 | if (left is float fa && right is float fb) 32 | { 33 | return ComparisonHelpers.AreEqualDouble(fa, fb, context); 34 | } 35 | 36 | if (left is decimal m1 && right is decimal m2) 37 | { 38 | return ComparisonHelpers.AreEqualDecimal(m1, m2, context); 39 | } 40 | 41 | if (IsNumeric(left) && IsNumeric(right)) 42 | { 43 | return NumericEqual(left, right, context); 44 | } 45 | 46 | var typeLeft = left.GetType(); 47 | var typeRight = right.GetType(); 48 | if (typeLeft != typeRight) 49 | { 50 | return false; 51 | } 52 | 53 | if (GeneratedHelperRegistry.TryCompare(left, right, context, out var eqFromRegistry)) 54 | { 55 | return eqFromRegistry; 56 | } 57 | 58 | if (left is IDictionary sdictA && right is IDictionary sdictB) 59 | { 60 | return EqualStringObjectDictionary(sdictA, sdictB, context); 61 | } 62 | 63 | if (left is IDictionary dictA && right is IDictionary dictB) 64 | { 65 | return EqualNonGenericDictionary(dictA, dictB, context); 66 | } 67 | 68 | if (left is Array arrA && right is Array arrB) 69 | { 70 | if (arrA.Length != arrB.Length) 71 | { 72 | return false; 73 | } 74 | 75 | for (var i = 0; i < arrA.Length; i++) 76 | { 77 | if (!AreEqualDynamic(arrA.GetValue(i), arrB.GetValue(i), context)) 78 | { 79 | return false; 80 | } 81 | } 82 | return true; 83 | } 84 | 85 | if (left is IEnumerable seqA && right is IEnumerable seqB) 86 | { 87 | return EqualNonGenericSequence(seqA, seqB, context); 88 | } 89 | 90 | if (IsPrimitiveLike(left)) 91 | { 92 | return left.Equals(right); 93 | } 94 | 95 | return left.Equals(right); 96 | } 97 | 98 | private static bool EqualStringObjectDictionary( 99 | IDictionary a, 100 | IDictionary b, 101 | ComparisonContext context) 102 | { 103 | if (a.Count != b.Count) 104 | { 105 | return false; 106 | } 107 | 108 | foreach (var kvp in a) 109 | { 110 | if (!b.TryGetValue(kvp.Key, out var rb)) 111 | { 112 | return false; 113 | } 114 | 115 | if (!AreEqualDynamic(kvp.Value, rb, context)) 116 | { 117 | return false; 118 | } 119 | } 120 | return true; 121 | } 122 | 123 | private static bool EqualNonGenericDictionary(IDictionary a, IDictionary b, ComparisonContext context) 124 | { 125 | if (a.Count != b.Count) 126 | { 127 | return false; 128 | } 129 | 130 | foreach (DictionaryEntry de in a) 131 | { 132 | if (!b.Contains(de.Key)) 133 | { 134 | return false; 135 | } 136 | 137 | var rv = b[de.Key]; 138 | if (!AreEqualDynamic(de.Value, rv, context)) 139 | { 140 | return false; 141 | } 142 | } 143 | return true; 144 | } 145 | 146 | private static bool EqualNonGenericSequence(IEnumerable a, IEnumerable b, ComparisonContext context) 147 | { 148 | var ea = a.GetEnumerator(); 149 | var eb = b.GetEnumerator(); 150 | while (true) 151 | { 152 | var ma = ea.MoveNext(); 153 | var mb = eb.MoveNext(); 154 | if (ma != mb) 155 | { 156 | return false; 157 | } 158 | 159 | if (!ma) 160 | { 161 | return true; 162 | } 163 | 164 | if (!AreEqualDynamic(ea.Current, eb.Current, context)) 165 | { 166 | return false; 167 | } 168 | } 169 | } 170 | 171 | private static bool IsPrimitiveLike(object v) 172 | => v is bool 173 | || v is byte || v is sbyte 174 | || v is short || v is ushort 175 | || v is int || v is uint 176 | || v is long || v is ulong 177 | || v is char 178 | || v is Guid 179 | || v is DateTime || v is DateTimeOffset || v is TimeSpan 180 | || v.GetType().IsEnum; 181 | 182 | private static bool IsNumeric(object o) => 183 | o is byte or sbyte or short or ushort or int or uint or long or ulong 184 | or float or double or decimal; 185 | 186 | private static bool NumericEqual(object a, object b, ComparisonContext context) 187 | { 188 | if (a is float or double or decimal || b is float or double or decimal) 189 | { 190 | var da = a is decimal mad ? (double)mad : Convert.ToDouble(a); 191 | var db = b is decimal mbd ? (double)mbd : Convert.ToDouble(b); 192 | return ComparisonHelpers.AreEqualDouble(da, db, context); 193 | } 194 | 195 | var va = Convert.ToDecimal(a); 196 | var vb = Convert.ToDecimal(b); 197 | return va == vb; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Tests/RefTypeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | using DeepEqual.Generator.Tests.Models; 3 | 4 | namespace DeepEqual.Generator.Tests.Tests; 5 | 6 | public class RefTypeTests 7 | { 8 | [Fact] 9 | public void Strings_Case_Sensitive_By_Default() 10 | { 11 | var a = new StringHolder { Value = "Hello" }; 12 | var b = new StringHolder { Value = "Hello" }; 13 | var c = new StringHolder { Value = "hello" }; 14 | Assert.True(StringHolderDeepEqual.AreDeepEqual(a, b)); 15 | Assert.False(StringHolderDeepEqual.AreDeepEqual(a, c)); 16 | } 17 | 18 | [Fact] 19 | public void Nullable_String_Null_Handling() 20 | { 21 | var n1 = new NullableStringHolder { Value = null }; 22 | var n2 = new NullableStringHolder { Value = null }; 23 | var v = new NullableStringHolder { Value = "x" }; 24 | Assert.True(NullableStringHolderDeepEqual.AreDeepEqual(n1, n2)); 25 | Assert.False(NullableStringHolderDeepEqual.AreDeepEqual(n1, v)); 26 | } 27 | 28 | [Fact] 29 | public void Deep_Shallow_Reference_Skip_Semantics() 30 | { 31 | var deepLeft = new Item { X = 1, Name = "x" }; 32 | var deepRight = new Item { X = 1, Name = "x" }; 33 | 34 | var shallowLeft = new Item { X = 2, Name = "y" }; 35 | var shallowRightSameValues = new Item { X = 2, Name = "y" }; 36 | var shallowSameRef = shallowLeft; 37 | 38 | var refLeft = new Item { X = 3, Name = "z" }; 39 | var refRightDifferentRefSameValues = new Item { X = 3, Name = "z" }; 40 | var refSameRef = refLeft; 41 | 42 | var skipLeft = new Item { X = 9, Name = "q" }; 43 | var skipRightDifferent = new Item { X = 123, Name = "QQ" }; 44 | 45 | var a = new MemberKindContainer 46 | { 47 | ValDeep = deepLeft, 48 | ValShallow = shallowLeft, 49 | ValReference = refLeft, 50 | ValSkipped = skipLeft 51 | }; 52 | var b = new MemberKindContainer 53 | { 54 | ValDeep = deepRight, 55 | ValShallow = shallowRightSameValues, 56 | ValReference = refRightDifferentRefSameValues, 57 | ValSkipped = skipRightDifferent 58 | }; 59 | 60 | Assert.False(MemberKindContainerDeepEqual.AreDeepEqual(a, b)); 61 | 62 | b.ValShallow = shallowSameRef; 63 | Assert.False(MemberKindContainerDeepEqual.AreDeepEqual(a, b)); 64 | 65 | b.ValReference = refSameRef; 66 | Assert.True(MemberKindContainerDeepEqual.AreDeepEqual(a, b)); 67 | } 68 | 69 | [Fact] 70 | public void Type_Level_Shallow_On_Member_Type() 71 | { 72 | var a = new ContainerWithTypeLevelShallow { Child = new TypeLevelShallowChild { V = 5 } }; 73 | var b = new ContainerWithTypeLevelShallow { Child = new TypeLevelShallowChild { V = 5 } }; 74 | Assert.False(ContainerWithTypeLevelShallowDeepEqual.AreDeepEqual(a, b)); 75 | var same = a; 76 | Assert.True(ContainerWithTypeLevelShallowDeepEqual.AreDeepEqual(a, same)); 77 | } 78 | 79 | [Fact] 80 | public void Include_Internals_Controls_Visibility() 81 | { 82 | var inc1 = new WithInternalsIncluded { Shown = 1, Hidden = 2 }; 83 | var inc2 = new WithInternalsIncluded { Shown = 1, Hidden = 99 }; 84 | Assert.False(WithInternalsIncludedDeepEqual.AreDeepEqual(inc1, inc2)); 85 | 86 | var exc1 = new WithInternalsExcluded { Shown = 1, Hidden = 2 }; 87 | var exc2 = new WithInternalsExcluded { Shown = 1, Hidden = 99 }; 88 | Assert.True(WithInternalsExcludedDeepEqual.AreDeepEqual(exc1, exc2)); 89 | } 90 | 91 | [Fact] 92 | public void Only_Members_Included_Or_Ignored_Are_Respected() 93 | { 94 | var i1 = new OnlySomeMembers { A = 1, B = 2, C = 777 }; 95 | var i2 = new OnlySomeMembers { A = 1, B = 2, C = -1 }; 96 | Assert.True(OnlySomeMembersDeepEqual.AreDeepEqual(i1, i2)); 97 | 98 | var j1 = new IgnoreSomeMembers { A = 7, B = 8, C = 100 }; 99 | var j2 = new IgnoreSomeMembers { A = 7, B = 8, C = -100 }; 100 | Assert.True(IgnoreSomeMembersDeepEqual.AreDeepEqual(j1, j2)); 101 | 102 | var j3 = new IgnoreSomeMembers { A = 7, B = 9, C = 100 }; 103 | Assert.False(IgnoreSomeMembersDeepEqual.AreDeepEqual(j1, j3)); 104 | } 105 | 106 | [Fact] 107 | public void Object_Member_Uses_Registry_When_Runtime_Type_Is_Registered() 108 | { 109 | var a = new ObjectHolder { Known = new ChildRef { Value = 10 }, Any = new ChildRef { Value = 10 } }; 110 | var b = new ObjectHolder { Known = new ChildRef { Value = 10 }, Any = new ChildRef { Value = 10 } }; 111 | Assert.True(ObjectHolderDeepEqual.AreDeepEqual(a, b)); 112 | 113 | var c = new ObjectHolder { Known = new ChildRef { Value = 10 }, Any = new ChildRef { Value = 99 } }; 114 | Assert.False(ObjectHolderDeepEqual.AreDeepEqual(a, c)); 115 | } 116 | 117 | [Fact] 118 | public void Object_Member_Falls_Back_When_Runtime_Type_Is_Not_Registered() 119 | { 120 | var a = new ObjectHolder { Known = new ChildRef { Value = 1 }, Any = new Unregistered { V = 1 } }; 121 | var b = new ObjectHolder { Known = new ChildRef { Value = 1 }, Any = new Unregistered { V = 1 } }; 122 | Assert.False(ObjectHolderDeepEqual.AreDeepEqual(a, b)); 123 | var shared = new Unregistered { V = 1 }; 124 | var c = new ObjectHolder { Known = new ChildRef { Value = 1 }, Any = shared }; 125 | var d = new ObjectHolder { Known = new ChildRef { Value = 1 }, Any = shared }; 126 | Assert.True(ObjectHolderDeepEqual.AreDeepEqual(c, d)); 127 | } 128 | 129 | [Fact] 130 | public void Cycle_Tracking_Prevents_Stack_Overflow_And_Compares_Correctly() 131 | { 132 | var a1 = new CycleNode { Id = 1 }; 133 | var a2 = new CycleNode { Id = 2 }; 134 | a1.Next = a2; 135 | a2.Next = a1; 136 | 137 | var b1 = new CycleNode { Id = 1 }; 138 | var b2 = new CycleNode { Id = 2 }; 139 | b1.Next = b2; 140 | b2.Next = b1; 141 | 142 | Assert.True(CycleNodeDeepEqual.AreDeepEqual(a1, b1)); 143 | 144 | b2.Id = 99; 145 | Assert.False(CycleNodeDeepEqual.AreDeepEqual(a1, b1)); 146 | } 147 | 148 | [Fact] 149 | public void Base_Members_Included_And_Excluded() 150 | { 151 | var a = new DerivedWithBaseIncluded { BaseId = "B1", Name = "X" }; 152 | var b = new DerivedWithBaseIncluded { BaseId = "B1", Name = "X" }; 153 | Assert.True(DerivedWithBaseIncludedDeepEqual.AreDeepEqual(a, b)); 154 | 155 | var bDiff = new DerivedWithBaseIncluded { BaseId = "DIFF", Name = "X" }; 156 | Assert.False(DerivedWithBaseIncludedDeepEqual.AreDeepEqual(a, bDiff)); 157 | 158 | var c = new DerivedWithBaseExcluded { BaseId = "B1", Name = "X" }; 159 | var d = new DerivedWithBaseExcluded { BaseId = "DIFF", Name = "X" }; 160 | Assert.True(DerivedWithBaseExcludedDeepEqual.AreDeepEqual(c, d)); 161 | } 162 | 163 | [Fact] 164 | public void Custom_Comparer_On_Member_Works() 165 | { 166 | var a = new CustomComparerHolder { Code = "ABc" }; 167 | var b = new CustomComparerHolder { Code = "abc" }; 168 | Assert.True(CustomComparerHolderDeepEqual.AreDeepEqual(a, b)); 169 | } 170 | 171 | [Fact] 172 | public void Numeric_Custom_Comparer_Works() 173 | { 174 | var a = new NumericWithComparer { Value = 1.0000001 }; 175 | var b = new NumericWithComparer { Value = 1.0000002 }; 176 | Assert.True(NumericWithComparerDeepEqual.AreDeepEqual(a, b)); 177 | } 178 | 179 | [Fact] 180 | public void Memory_And_ReadOnlyMemory_Are_Compared_By_Content() 181 | { 182 | var bytes1 = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray(); 183 | var bytes2 = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray(); 184 | var bytes3 = Enumerable.Range(0, 32).Select(i => (byte)(i + 1)).ToArray(); 185 | 186 | var a = new MemoryHolder { Buf = new Memory(bytes1), RBuf = new ReadOnlyMemory(bytes2) }; 187 | var b = new MemoryHolder { Buf = new Memory(bytes1.ToArray()), RBuf = new ReadOnlyMemory(bytes2.ToArray()) }; 188 | var c = new MemoryHolder { Buf = new Memory(bytes3), RBuf = new ReadOnlyMemory(bytes2) }; 189 | 190 | Assert.True(MemoryHolderDeepEqual.AreDeepEqual(a, b)); 191 | Assert.False(MemoryHolderDeepEqual.AreDeepEqual(a, c)); 192 | } 193 | 194 | [Fact] 195 | public void Memory_And_ReadOnlyMemory_Slices_And_Defaults() 196 | { 197 | var base1 = new byte[] { 0, 1, 2, 3, 4, 5 }; 198 | var base2 = new byte[] { 0, 1, 2, 3, 4, 5 }; 199 | var base3 = new byte[] { 0, 1, 9, 3, 4, 5 }; 200 | 201 | var a = new MemoryHolder { Buf = new Memory(base1).Slice(2, 2), RBuf = new ReadOnlyMemory(base1).Slice(1, 3) }; 202 | var b = new MemoryHolder { Buf = new Memory(base2).Slice(2, 2), RBuf = new ReadOnlyMemory(base2).Slice(1, 3) }; 203 | var c = new MemoryHolder { Buf = new Memory(base3).Slice(2, 2), RBuf = new ReadOnlyMemory(base3).Slice(1, 3) }; 204 | 205 | Assert.True(MemoryHolderDeepEqual.AreDeepEqual(a, b)); 206 | Assert.False(MemoryHolderDeepEqual.AreDeepEqual(a, c)); 207 | 208 | var d = new MemoryHolder { Buf = default, RBuf = default }; 209 | var e = new MemoryHolder { Buf = default, RBuf = default }; 210 | Assert.True(MemoryHolderDeepEqual.AreDeepEqual(d, e)); 211 | } 212 | 213 | [Fact] 214 | public void Dynamics_Expando_Missing_And_Nested_Diff() 215 | { 216 | dynamic e1 = new ExpandoObject(); 217 | e1.id = 1; 218 | e1.name = "x"; 219 | e1.arr = new[] { 1, 2, 3 }; 220 | e1.map = new Dictionary { ["k"] = 1, ["z"] = new[] { "p", "q" } }; 221 | 222 | dynamic e2 = new ExpandoObject(); 223 | e2.id = 1; 224 | e2.name = "x"; 225 | e2.arr = new[] { 1, 2, 3 }; 226 | e2.map = new Dictionary { ["k"] = 1, ["z"] = new[] { "p", "q" } }; 227 | 228 | var a = new DynamicHolder { Data = (IDictionary)e1 }; 229 | var b = new DynamicHolder { Data = (IDictionary)e2 }; 230 | Assert.True(DynamicHolderDeepEqual.AreDeepEqual(a, b)); 231 | 232 | ((IDictionary)e2).Remove("name"); 233 | Assert.False(DynamicHolderDeepEqual.AreDeepEqual(a, b)); 234 | 235 | ((IDictionary)e2)["name"] = "x"; 236 | ((Dictionary)e2.map)["z"] = new[] { "p", "Q" }; 237 | Assert.False(DynamicHolderDeepEqual.AreDeepEqual(a, b)); 238 | } 239 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Tests/Tests/CollectionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | using DeepEqual.Generator.Shared; 3 | using DeepEqual.Generator.Tests.Models; 4 | 5 | namespace DeepEqual.Generator.Tests.Tests; 6 | 7 | public class CollectionsTests 8 | { 9 | [Fact] 10 | public void Array_Ordered_By_Default() 11 | { 12 | var a = new RootOrderSensitiveCollections { Names = new List { "a", "b" } }; 13 | var b = new RootOrderSensitiveCollections { Names = new List { "b", "a" } }; 14 | Assert.False(RootOrderSensitiveCollectionsDeepEqual.AreDeepEqual(a, b)); 15 | } 16 | 17 | [Fact] 18 | public void Member_Level_OrderInsensitive_For_Array_Or_List() 19 | { 20 | var a = new WithOrderInsensitiveMember { Values = new List { 1, 2, 2, 3 } }; 21 | var b = new WithOrderInsensitiveMember { Values = new List { 2, 3, 2, 1 } }; 22 | Assert.True(WithOrderInsensitiveMemberDeepEqual.AreDeepEqual(a, b)); 23 | var c = new WithOrderInsensitiveMember { Values = new List { 1, 2, 3 } }; 24 | Assert.False(WithOrderInsensitiveMemberDeepEqual.AreDeepEqual(a, c)); 25 | } 26 | 27 | [Fact] 28 | public void Root_Level_OrderInsensitive_Applies_To_Collections() 29 | { 30 | var a = new RootOrderInsensitiveCollections 31 | { 32 | Names = new List { "x", "y", "y" }, 33 | People = new List { new() { Name = "p1", Age = 1 }, new() { Name = "p2", Age = 2 } }, 34 | ForcedOrdered = new List { 1, 2, 3 } 35 | }; 36 | var b = new RootOrderInsensitiveCollections 37 | { 38 | Names = new List { "y", "x", "y" }, 39 | People = new List { new() { Name = "p2", Age = 2 }, new() { Name = "p1", Age = 1 } }, 40 | ForcedOrdered = new List { 1, 2, 3 } 41 | }; 42 | Assert.True(RootOrderInsensitiveCollectionsDeepEqual.AreDeepEqual(a, b)); 43 | 44 | Assert.False(RootOrderInsensitiveCollectionsDeepEqual.AreDeepEqual( 45 | new RootOrderInsensitiveCollections { ForcedOrdered = new List { 1, 2, 3 } }, 46 | new RootOrderInsensitiveCollections { ForcedOrdered = new List { 3, 2, 1 } })); 47 | } 48 | 49 | [Fact] 50 | public void Element_Type_OrderInsensitive_Default_Is_Respected() 51 | { 52 | var a = new RootWithElementTypeDefaultUnordered 53 | { 54 | Tags = new List 55 | { 56 | new() { Label = "A" }, 57 | new() { Label = "B" } 58 | } 59 | }; 60 | var b = new RootWithElementTypeDefaultUnordered 61 | { 62 | Tags = new List 63 | { 64 | new() { Label = "B" }, 65 | new() { Label = "A" } 66 | } 67 | }; 68 | Assert.True(RootWithElementTypeDefaultUnorderedDeepEqual.AreDeepEqual(a, b)); 69 | } 70 | 71 | [Fact] 72 | public void MultiDimensional_Arrays_Are_Compared_By_Shape_And_Values() 73 | { 74 | var m1 = new MultiDimArrayHolder { Matrix = new int[2, 2] { { 1, 2 }, { 3, 4 } } }; 75 | var m2 = new MultiDimArrayHolder { Matrix = new int[2, 2] { { 1, 2 }, { 3, 4 } } }; 76 | var m3 = new MultiDimArrayHolder { Matrix = new int[2, 2] { { 1, 2 }, { 4, 3 } } }; 77 | var m4 = new MultiDimArrayHolder { Matrix = new int[2, 3] { { 1, 2, 0 }, { 3, 4, 0 } } }; 78 | Assert.True(MultiDimArrayHolderDeepEqual.AreDeepEqual(m1, m2)); 79 | Assert.False(MultiDimArrayHolderDeepEqual.AreDeepEqual(m1, m3)); 80 | Assert.False(MultiDimArrayHolderDeepEqual.AreDeepEqual(m1, m4)); 81 | } 82 | 83 | [Fact] 84 | public void Dictionaries_Use_Deep_Compare_For_Values() 85 | { 86 | var a = new DictionaryHolder 87 | { 88 | Map = new Dictionary 89 | { 90 | [1] = new() { Name = "A", Age = 10 }, 91 | [2] = new() { Name = "B", Age = 20 } 92 | } 93 | }; 94 | var b = new DictionaryHolder 95 | { 96 | Map = new Dictionary 97 | { 98 | [2] = new() { Name = "B", Age = 20 }, 99 | [1] = new() { Name = "A", Age = 10 } 100 | } 101 | }; 102 | var c = new DictionaryHolder 103 | { 104 | Map = new Dictionary 105 | { 106 | [1] = new() { Name = "A", Age = 10 }, 107 | [2] = new() { Name = "B", Age = 99 } 108 | } 109 | }; 110 | Assert.True(DictionaryHolderDeepEqual.AreDeepEqual(a, b)); 111 | Assert.False(DictionaryHolderDeepEqual.AreDeepEqual(a, c)); 112 | } 113 | 114 | [Fact] 115 | public void Unordered_List_Of_Objects_Without_Keys_Still_Equal_When_Swapped() 116 | { 117 | var a = new CustomersUnkeyed 118 | { 119 | People = new List 120 | { 121 | new() { Name = "p1", Age = 1 }, 122 | new() { Name = "p2", Age = 2 } 123 | } 124 | }; 125 | var b = new CustomersUnkeyed 126 | { 127 | People = new List 128 | { 129 | new() { Name = "p2", Age = 2 }, 130 | new() { Name = "p1", Age = 1 } 131 | } 132 | }; 133 | Assert.True(CustomersUnkeyedDeepEqual.AreDeepEqual(a, b)); 134 | } 135 | 136 | [Fact] 137 | public void Unordered_List_With_KeyMembers_Matches_By_Key() 138 | { 139 | var a = new CustomersKeyed 140 | { 141 | Customers = new List 142 | { 143 | new() { Id = "a", Name = "alice" }, 144 | new() { Id = "b", Name = "bob" } 145 | } 146 | }; 147 | var b = new CustomersKeyed 148 | { 149 | Customers = new List 150 | { 151 | new() { Id = "b", Name = "bob" }, 152 | new() { Id = "a", Name = "alice" } 153 | } 154 | }; 155 | Assert.True(CustomersKeyedDeepEqual.AreDeepEqual(a, b)); 156 | 157 | var c = new CustomersKeyed 158 | { 159 | Customers = new List 160 | { 161 | new() { Id = "a", Name = "alice" }, 162 | new() { Id = "b", Name = "BOB!" } 163 | } 164 | }; 165 | Assert.False(CustomersKeyedDeepEqual.AreDeepEqual(a, c)); 166 | 167 | var d = new CustomersKeyed 168 | { 169 | Customers = new List 170 | { 171 | new() { Id = "a", Name = "alice" } 172 | } 173 | }; 174 | Assert.False(CustomersKeyedDeepEqual.AreDeepEqual(a, d)); 175 | } 176 | 177 | 178 | [Fact] 179 | public void Object_Array_Uses_Structural_Equality() 180 | { 181 | var a = new ObjArr { Any = new[] { 1, 2, 3 } }; 182 | var b = new ObjArr { Any = new[] { 1, 2, 3 } }; 183 | var c = new ObjArr { Any = new[] { 1, 2, 4 } }; 184 | Assert.True(ObjArrDeepEqual.AreDeepEqual(a, b)); 185 | Assert.False(ObjArrDeepEqual.AreDeepEqual(a, c)); 186 | } 187 | 188 | [Fact] 189 | public void Interface_Property_Uses_Runtime_Type() 190 | { 191 | var a = new Zoo { Animal = new Cat { Age = 3, Name = "Paws" } }; 192 | var b = new Zoo { Animal = new Cat { Age = 3, Name = "Paws" } }; 193 | var c = new Zoo { Animal = new Cat { Age = 3, Name = "Claws" } }; 194 | Assert.True(ZooDeepEqual.AreDeepEqual(a, b)); 195 | Assert.False(ZooDeepEqual.AreDeepEqual(a, c)); 196 | } 197 | 198 | [Fact] 199 | public void Jagged_Vs_Multidimensional_Arrays_NotEqual() 200 | { 201 | var jagged = new int[][] { new[] { 1, 2 }, new[] { 3, 4 } }; 202 | var multi = new int[2, 2] { { 1, 2 }, { 3, 4 } }; 203 | var a = new ArrayHolder { Any = jagged }; 204 | var b = new ArrayHolder { Any = multi }; 205 | Assert.False(ArrayHolderDeepEqual.AreDeepEqual(a, b)); 206 | } 207 | 208 | [Fact] 209 | public void Unordered_Keyed_With_Duplicates_Must_Match_PerBucket_Count_And_Values() 210 | { 211 | var a = new KeyedBag { Items = new() { new() { Name = "x", X = 1 }, new() { Name = "x", X = 2 }, new() { Name = "y", X = 9 } } }; 212 | var b = new KeyedBag { Items = new() { new() { Name = "x", X = 2 }, new() { Name = "y", X = 9 }, new() { Name = "x", X = 1 } } }; 213 | var c = new KeyedBag { Items = new() { new() { Name = "x", X = 1 }, new() { Name = "y", X = 9 } } }; Assert.True(KeyedBagDeepEqual.AreDeepEqual(a, b)); 214 | Assert.False(KeyedBagDeepEqual.AreDeepEqual(a, c)); 215 | } 216 | 217 | [Fact] 218 | public void String_And_Number_Options_Are_Respected() 219 | { 220 | var a = new OptsHolder { S = "Hello", D = 1.0000001, M = 1.00005m }; 221 | var b = new OptsHolder { S = "hello", D = 1.0000002, M = 1.00001m }; 222 | 223 | var loose = new ComparisonOptions 224 | { 225 | StringComparison = StringComparison.OrdinalIgnoreCase, 226 | DoubleEpsilon = 1e-6, 227 | DecimalEpsilon = 0.0001m 228 | }; 229 | Assert.True(OptsHolderDeepEqual.AreDeepEqual(a, b, loose)); 230 | 231 | var strict = new ComparisonOptions 232 | { 233 | StringComparison = StringComparison.Ordinal, 234 | DoubleEpsilon = 0.0, 235 | DecimalEpsilon = 0m 236 | }; 237 | Assert.False(OptsHolderDeepEqual.AreDeepEqual(a, b, strict)); 238 | } 239 | 240 | [DeepComparable] public sealed class OptsHolder { public string? S { get; init; } public double D { get; init; } public decimal M { get; init; } } 241 | 242 | [Fact] 243 | public void Polymorphism_In_Array_And_ReadOnlyList() 244 | { 245 | var arrA = new ZooArray { Animals = new IAnimal[] { new Cat { Age = 4, Name = "Rex" } } }; 246 | var arrB = new ZooArray { Animals = new IAnimal[] { new Cat { Age = 4, Name = "Rex" } } }; 247 | var arrC = new ZooArray { Animals = new IAnimal[] { new Cat { Age = 5, Name = "Rex" } } }; 248 | Assert.True(ZooArrayDeepEqual.AreDeepEqual(arrA, arrB)); 249 | Assert.False(ZooArrayDeepEqual.AreDeepEqual(arrA, arrC)); 250 | 251 | var roA = new ZooRoList { Animals = new List { new Cat { Age = 1, Name = "Kit" }, new Cat { Age = 3, Name = "Mog" } } }; 252 | var roB = new ZooRoList { Animals = new List { new Cat { Age = 1, Name = "Kit" }, new Cat { Age = 3, Name = "Mog" } } }; 253 | var roC = new ZooRoList { Animals = new List { new Cat { Age = 1, Name = "Kit" }, new Cat { Age = 2, Name = "Mog" } } }; 254 | Assert.True(ZooRoListDeepEqual.AreDeepEqual(roA, roB)); 255 | Assert.False(ZooRoListDeepEqual.AreDeepEqual(roA, roC)); 256 | } 257 | 258 | [Fact] 259 | public void Polymorphic_Collections_Handle_Nulls() 260 | { 261 | var a = new ZooList { Animals = new() { new Cat { Age = 1, Name = "A" }, null } }; 262 | var b = new ZooList { Animals = new() { new Cat { Age = 1, Name = "A" }, null } }; 263 | var c = new ZooList { Animals = new() { new Cat { Age = 1, Name = "A" }, new Cat { Age = 2, Name = "B" } } }; 264 | Assert.True(ZooListDeepEqual.AreDeepEqual(a, b)); 265 | Assert.False(ZooListDeepEqual.AreDeepEqual(a, c)); 266 | } 267 | 268 | [Fact] 269 | public void Dictionary_Value_Polymorphism_And_Type_Mismatch() 270 | { 271 | var a = new ZooDict { Pets = new() { ["a"] = new Cat { Age = 2, Name = "C" }, ["b"] = new Cat { Age = 5, Name = "D" } } }; 272 | var b = new ZooDict { Pets = new() { ["a"] = new Cat { Age = 2, Name = "C" }, ["b"] = new Cat { Age = 5, Name = "D" } } }; 273 | var c = new ZooDict { Pets = new() { ["a"] = new Cat { Age = 2, Name = "C" }, ["b"] = new Cat { Age = 6, Name = "D" } } }; 274 | Assert.True(ZooDictDeepEqual.AreDeepEqual(a, b)); 275 | Assert.False(ZooDictDeepEqual.AreDeepEqual(a, c)); 276 | 277 | var d = new ZooDict { Pets = new() { ["a"] = new Cat { Age = 2, Name = "C" }, ["b"] = new Dog { Age = 5, Name = "D" } } }; 278 | Assert.False(ZooDictDeepEqual.AreDeepEqual(a, d)); 279 | } 280 | 281 | [Fact] 282 | public void ReadOnlyDictionary_Wraps_Are_Equal() 283 | { 284 | var baseMap = new Dictionary { ["x"] = new Cat { Age = 1, Name = "A" } }; 285 | var r1 = new ZooRoDict { Pets = new System.Collections.ObjectModel.ReadOnlyDictionary(baseMap) }; 286 | var r2 = new ZooRoDict { Pets = new System.Collections.ObjectModel.ReadOnlyDictionary(new Dictionary(baseMap)) }; 287 | Assert.True(ZooRoDictDeepEqual.AreDeepEqual(r1, r2)); 288 | } 289 | 290 | [Fact] 291 | public void Dictionary_Key_Comparer_Case_Sensitivity() 292 | { 293 | var a1 = new Dictionary(StringComparer.Ordinal) { ["a"] = new Cat { Age = 1, Name = "X" } }; 294 | var a2 = new Dictionary(StringComparer.Ordinal) { ["A"] = new Cat { Age = 1, Name = "X" } }; 295 | var b1 = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["a"] = new Cat { Age = 1, Name = "X" } }; 296 | var b2 = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["A"] = new Cat { Age = 1, Name = "X" } }; 297 | 298 | Assert.False(ZooDictDeepEqual.AreDeepEqual(new ZooDict { Pets = a1 }, new ZooDict { Pets = a2 })); 299 | Assert.True(ZooDictDeepEqual.AreDeepEqual(new ZooDict { Pets = b1 }, new ZooDict { Pets = b2 })); 300 | } 301 | 302 | [Fact] 303 | public void Unordered_List_With_Composite_KeyMembers() 304 | { 305 | var a = new CompositeKeyBag { Items = new() { new Item { Name = "a", X = 1 }, new Item { Name = "b", X = 2 } } }; 306 | var b = new CompositeKeyBag { Items = new() { new Item { Name = "b", X = 2 }, new Item { Name = "a", X = 1 } } }; 307 | var c = new CompositeKeyBag { Items = new() { new Item { Name = "b", X = 99 }, new Item { Name = "a", X = 1 } } }; 308 | Assert.True(CompositeKeyBagDeepEqual.AreDeepEqual(a, b)); 309 | Assert.False(CompositeKeyBagDeepEqual.AreDeepEqual(a, c)); 310 | } 311 | 312 | [Fact] 313 | public void IEnumerable_List_Array_Content_Equality() 314 | { 315 | var list = new List { 1, 2, 3 }; 316 | var arr = new[] { 1, 2, 3 }; 317 | var a = new EnumerableHolder { Seq = list }; 318 | var b = new EnumerableHolder { Seq = arr }; 319 | Assert.True(EnumerableHolderDeepEqual.AreDeepEqual(a, b)); 320 | } 321 | } -------------------------------------------------------------------------------- /DeepEqual.Generator.Shared/ComparisonHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DeepEqual.Generator.Shared; 5 | 6 | public static class ComparisonHelpers 7 | { 8 | public static bool AreEqualStrings(string? a, string? b, ComparisonContext? context) 9 | { 10 | var comp = context?.Options.StringComparison ?? StringComparison.Ordinal; 11 | return string.Equals(a, b, comp); 12 | } 13 | 14 | public static bool AreEqualEnum(T a, T b) where T : struct, Enum => EqualityComparer.Default.Equals(a, b); 15 | 16 | public static bool AreEqualDateTime(DateTime a, DateTime b) => a.Kind == b.Kind && a.Ticks == b.Ticks; 17 | 18 | public static bool AreEqualDateTimeOffset(DateTimeOffset a, DateTimeOffset b) => a.Offset == b.Offset && a.UtcTicks == b.UtcTicks; 19 | 20 | public static bool AreEqualDateOnly(DateOnly a, DateOnly b) => a.DayNumber == b.DayNumber; 21 | 22 | public static bool AreEqualTimeOnly(TimeOnly a, TimeOnly b) => a.Ticks == b.Ticks; 23 | 24 | public static bool AreEqualSingle(float a, float b, ComparisonContext? context) 25 | { 26 | if (float.IsNaN(a) || float.IsNaN(b)) 27 | { 28 | return context?.Options.TreatNaNEqual == true && float.IsNaN(a) && float.IsNaN(b); 29 | } 30 | 31 | var eps = context?.Options.FloatEpsilon ?? 0f; 32 | if (eps <= 0f) 33 | { 34 | return a.Equals(b); 35 | } 36 | 37 | return Math.Abs(a - b) <= eps; 38 | } 39 | 40 | public static bool AreEqualDouble(double a, double b, ComparisonContext? context) 41 | { 42 | if (double.IsNaN(a) || double.IsNaN(b)) 43 | { 44 | return context?.Options.TreatNaNEqual == true && double.IsNaN(a) && double.IsNaN(b); 45 | } 46 | 47 | var eps = context?.Options.DoubleEpsilon ?? 0.0; 48 | if (eps <= 0.0) 49 | { 50 | return a.Equals(b); 51 | } 52 | 53 | return Math.Abs(a - b) <= eps; 54 | } 55 | 56 | public static bool AreEqualDecimal(decimal a, decimal b, ComparisonContext? context) 57 | { 58 | var eps = context?.Options.DecimalEpsilon ?? 0m; 59 | if (eps <= 0m) 60 | { 61 | return a.Equals(b); 62 | } 63 | 64 | return Math.Abs(a - b) <= eps; 65 | } 66 | 67 | public static bool AreEqualArrayRank1(TElement[]? a, TElement[]? b, TComparer comparer, ComparisonContext context) 68 | where TComparer : IElementComparer 69 | { 70 | if (ReferenceEquals(a, b)) 71 | { 72 | return true; 73 | } 74 | 75 | if (a is null || b is null) 76 | { 77 | return false; 78 | } 79 | 80 | if (a.Length != b.Length) 81 | { 82 | return false; 83 | } 84 | 85 | for (var i = 0; i < a.Length; i++) 86 | { 87 | if (!comparer.Invoke(a[i], b[i], context)) 88 | { 89 | return false; 90 | } 91 | } 92 | return true; 93 | } 94 | 95 | public static bool AreEqualArray(Array? a, Array? b, TComparer comparer, ComparisonContext context) 96 | where TComparer : IElementComparer 97 | { 98 | if (ReferenceEquals(a, b)) 99 | { 100 | return true; 101 | } 102 | 103 | if (a is null || b is null) 104 | { 105 | return false; 106 | } 107 | 108 | if (a.Rank != b.Rank) 109 | { 110 | return false; 111 | } 112 | 113 | for (var d = 0; d < a.Rank; d++) 114 | { 115 | if (a.GetLength(d) != b.GetLength(d)) 116 | { 117 | return false; 118 | } 119 | } 120 | if (a.Length == 0) 121 | { 122 | return true; 123 | } 124 | 125 | var indices = new int[a.Rank]; 126 | while (true) 127 | { 128 | var va = (TElement)a.GetValue(indices)!; 129 | var vb = (TElement)b.GetValue(indices)!; 130 | if (!comparer.Invoke(va, vb, context)) 131 | { 132 | return false; 133 | } 134 | 135 | var dim = a.Rank - 1; 136 | while (dim >= 0) 137 | { 138 | indices[dim]++; 139 | if (indices[dim] < a.GetLength(dim)) 140 | { 141 | break; 142 | } 143 | 144 | indices[dim] = 0; 145 | dim--; 146 | } 147 | if (dim < 0) 148 | { 149 | break; 150 | } 151 | } 152 | return true; 153 | } 154 | 155 | public static bool AreEqualArrayUnordered(Array? a, Array? b, TComparer comparer, ComparisonContext context) 156 | where TComparer : IElementComparer 157 | { 158 | if (ReferenceEquals(a, b)) 159 | { 160 | return true; 161 | } 162 | 163 | if (a is null || b is null) 164 | { 165 | return false; 166 | } 167 | 168 | if (a.Length != b.Length) 169 | { 170 | return false; 171 | } 172 | 173 | var listA = new List(a.Length); 174 | var listB = new List(b.Length); 175 | foreach (var o in a) listA.Add((TElement)o!); 176 | foreach (var o in b) listB.Add((TElement)o!); 177 | return AreEqualUnordered(listA, listB, comparer, context); 178 | } 179 | 180 | public static bool AreEqualSequencesOrdered(IEnumerable? a, IEnumerable? b, TComparer comparer, ComparisonContext context) 181 | where TComparer : IElementComparer 182 | { 183 | if (ReferenceEquals(a, b)) 184 | { 185 | return true; 186 | } 187 | 188 | if (a is null || b is null) 189 | { 190 | return false; 191 | } 192 | 193 | using var ea = a.GetEnumerator(); 194 | using var eb = b.GetEnumerator(); 195 | while (true) 196 | { 197 | var ma = ea.MoveNext(); 198 | var mb = eb.MoveNext(); 199 | if (ma != mb) 200 | { 201 | return false; 202 | } 203 | 204 | if (!ma) 205 | { 206 | return true; 207 | } 208 | 209 | if (!comparer.Invoke(ea.Current, eb.Current, context)) 210 | { 211 | return false; 212 | } 213 | } 214 | } 215 | 216 | public static bool AreEqualSequencesUnordered(IEnumerable? a, IEnumerable? b, TComparer comparer, ComparisonContext context) 217 | where TComparer : IElementComparer 218 | { 219 | if (ReferenceEquals(a, b)) 220 | { 221 | return true; 222 | } 223 | 224 | if (a is null || b is null) 225 | { 226 | return false; 227 | } 228 | 229 | if (a is ICollection ca && b is ICollection cb && ca.Count != cb.Count) 230 | { 231 | return false; 232 | } 233 | 234 | var listA = new List(a); 235 | var listB = new List(b); 236 | return AreEqualUnordered(listA, listB, comparer, context); 237 | } 238 | 239 | public static bool AreEqualSequencesUnorderedHash(IEnumerable? a, IEnumerable? b, IEqualityComparer equalityComparer) where T : notnull 240 | { 241 | if (ReferenceEquals(a, b)) 242 | { 243 | return true; 244 | } 245 | 246 | if (a is null || b is null) 247 | { 248 | return false; 249 | } 250 | 251 | if (a is ICollection ca && b is ICollection cb && ca.Count != cb.Count) 252 | { 253 | return false; 254 | } 255 | 256 | var counts = new Dictionary(equalityComparer); 257 | foreach (var item in a) 258 | { 259 | counts.TryGetValue(item, out var n); 260 | counts[item] = n + 1; 261 | } 262 | foreach (var item in b) 263 | { 264 | if (!counts.TryGetValue(item, out var n)) 265 | { 266 | return false; 267 | } 268 | 269 | if (n == 1) 270 | { 271 | counts.Remove(item); 272 | } 273 | else 274 | { 275 | counts[item] = n - 1; 276 | } 277 | } 278 | return counts.Count == 0; 279 | } 280 | 281 | public static bool AreEqualReadOnlyMemory(ReadOnlyMemory a, ReadOnlyMemory b, TComparer comparer, ComparisonContext context) 282 | where TComparer : IElementComparer 283 | { 284 | if (a.Length != b.Length) 285 | { 286 | return false; 287 | } 288 | 289 | var sa = a.Span; 290 | var sb = b.Span; 291 | for (var i = 0; i < sa.Length; i++) 292 | { 293 | if (!comparer.Invoke(sa[i], sb[i], context)) 294 | { 295 | return false; 296 | } 297 | } 298 | return true; 299 | } 300 | 301 | public static bool AreEqualMemory(Memory a, Memory b, TComparer comparer, ComparisonContext context) 302 | where TComparer : IElementComparer 303 | { 304 | if (a.Length != b.Length) 305 | { 306 | return false; 307 | } 308 | 309 | var sa = a.Span; 310 | var sb = b.Span; 311 | for (var i = 0; i < sa.Length; i++) 312 | { 313 | if (!comparer.Invoke(sa[i], sb[i], context)) 314 | { 315 | return false; 316 | } 317 | } 318 | return true; 319 | } 320 | 321 | private static bool AreEqualUnordered(List a, List b, TComparer comparer, ComparisonContext context) 322 | where TComparer : IElementComparer 323 | { 324 | if (a.Count != b.Count) 325 | { 326 | return false; 327 | } 328 | 329 | if (a.Count == 0) 330 | { 331 | return true; 332 | } 333 | 334 | var matched = new bool[b.Count]; 335 | for (var i = 0; i < a.Count; i++) 336 | { 337 | var found = false; 338 | for (var j = 0; j < b.Count; j++) 339 | { 340 | if (matched[j]) 341 | { 342 | continue; 343 | } 344 | 345 | if (comparer.Invoke(a[i], b[j], context)) 346 | { 347 | matched[j] = true; 348 | found = true; 349 | break; 350 | } 351 | } 352 | if (!found) 353 | { 354 | return false; 355 | } 356 | } 357 | return true; 358 | } 359 | 360 | public static bool AreEqualDictionariesAny(object? a, object? b, TValueComparer comparer, ComparisonContext context) 361 | where TValueComparer : IElementComparer 362 | where TKey : notnull 363 | { 364 | if (ReferenceEquals(a, b)) 365 | { 366 | return true; 367 | } 368 | 369 | if (a is null || b is null) 370 | { 371 | return false; 372 | } 373 | 374 | if (a is IReadOnlyDictionary roa && b is IReadOnlyDictionary rob) 375 | { 376 | if (roa.Count != rob.Count) 377 | { 378 | return false; 379 | } 380 | 381 | foreach (var kv in roa) 382 | { 383 | if (!rob.TryGetValue(kv.Key, out var bv)) 384 | { 385 | return false; 386 | } 387 | 388 | if (!comparer.Invoke(kv.Value, bv, context)) 389 | { 390 | return false; 391 | } 392 | } 393 | return true; 394 | } 395 | if (a is IDictionary rwa && b is IDictionary rwb) 396 | { 397 | if (rwa.Count != rwb.Count) 398 | { 399 | return false; 400 | } 401 | 402 | foreach (var kv in rwa) 403 | { 404 | if (!rwb.TryGetValue(kv.Key, out var bv)) 405 | { 406 | return false; 407 | } 408 | 409 | if (!comparer.Invoke(kv.Value, bv, context)) 410 | { 411 | return false; 412 | } 413 | } 414 | return true; 415 | } 416 | return Equals(a, b); 417 | } 418 | 419 | public static bool DeepComparePolymorphic(T left, T right, ComparisonContext context) 420 | { 421 | if (typeof(T).IsValueType) 422 | { 423 | object ol = left!; 424 | object orr = right!; 425 | var tl = ol.GetType(); 426 | var tr = orr.GetType(); 427 | if (tl == tr) 428 | { 429 | if (GeneratedHelperRegistry.TryCompareSameType(tl, ol, orr, context, out var eqv)) 430 | { 431 | return eqv; 432 | } 433 | 434 | GeneratedHelperRegistry.WarmUp(tl); 435 | if (GeneratedHelperRegistry.TryCompareSameType(tl, ol, orr, context, out eqv)) 436 | { 437 | return eqv; 438 | } 439 | } 440 | 441 | return EqualityComparer.Default.Equals(left, right); 442 | } 443 | else 444 | { 445 | if (ReferenceEquals(left, right)) 446 | { 447 | return true; 448 | } 449 | 450 | if (left is null || right is null) 451 | { 452 | return false; 453 | } 454 | 455 | object ol = left; 456 | object orr = right; 457 | var tl = ol.GetType(); 458 | var tr = orr.GetType(); 459 | if (tl != tr) 460 | { 461 | return false; 462 | } 463 | 464 | if (tl != tr) 465 | { 466 | return false; 467 | } 468 | 469 | if (GeneratedHelperRegistry.TryCompareSameType(tl, ol, orr, context, out var eqv)) 470 | { 471 | return eqv; 472 | } 473 | 474 | GeneratedHelperRegistry.WarmUp(tl); 475 | if (GeneratedHelperRegistry.TryCompareSameType(tl, ol, orr, context, out eqv)) 476 | { 477 | return eqv; 478 | } 479 | 480 | return Equals(left, right); 481 | } 482 | } 483 | 484 | public static IEqualityComparer GetStringComparer(ComparisonContext? context) 485 | { 486 | var sc = context?.Options.StringComparison ?? StringComparison.Ordinal; 487 | return sc switch 488 | { 489 | StringComparison.Ordinal => StringComparer.Ordinal, 490 | StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase, 491 | StringComparison.InvariantCulture => StringComparer.InvariantCulture, 492 | StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase, 493 | StringComparison.CurrentCulture => StringComparer.CurrentCulture, 494 | StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase, 495 | _ => StringComparer.Ordinal 496 | }; 497 | } 498 | 499 | public sealed class StrictDateTimeComparer : IEqualityComparer 500 | { 501 | public static readonly StrictDateTimeComparer Instance = new(); 502 | public bool Equals(DateTime x, DateTime y) => x.Kind == y.Kind && x.Ticks == y.Ticks; 503 | public int GetHashCode(DateTime obj) => HashCode.Combine((int)obj.Kind, obj.Ticks); 504 | } 505 | 506 | public sealed class StrictDateTimeOffsetComparer : IEqualityComparer 507 | { 508 | public static readonly StrictDateTimeOffsetComparer Instance = new(); 509 | public bool Equals(DateTimeOffset x, DateTimeOffset y) => x.Offset == y.Offset && x.UtcTicks == y.UtcTicks; 510 | public int GetHashCode(DateTimeOffset obj) => HashCode.Combine(obj.Offset, obj.UtcTicks); 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /DeepEqual.Generator.Benchmarking/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | using BenchmarkDotNet.Attributes; 3 | using BenchmarkDotNet.Running; 4 | using DeepEqual.Generator.Shared; 5 | using Newtonsoft.Json.Linq; 6 | using System.Text.Json; 7 | using System.Text.Json.Nodes; 8 | using KellermanSoftware.CompareNetObjects; 9 | using FluentAssertions; 10 | 11 | namespace DeepEqual.Generator.Benchmarking 12 | { 13 | internal class Program 14 | { 15 | static void Main() => BenchmarkRunner.Run(); 16 | } 17 | 18 | public enum Region { NA, EU, APAC } 19 | 20 | [DeepComparable(CycleTracking = false)] 21 | public sealed class Address 22 | { 23 | public string Line1 { get; set; } = ""; 24 | public string City { get; set; } = ""; 25 | public string Postcode { get; set; } = ""; 26 | public string Country { get; set; } = ""; 27 | } 28 | 29 | [DeepComparable(CycleTracking = false)] 30 | public sealed class OrderLine 31 | { 32 | public string Sku { get; set; } = ""; 33 | public int Qty { get; set; } 34 | public decimal LineTotal { get; set; } 35 | } 36 | 37 | [DeepComparable(CycleTracking = false)] 38 | public sealed class Order 39 | { 40 | public Guid Id { get; set; } 41 | public DateTimeOffset Created { get; set; } 42 | public List Lines { get; set; } = new(); 43 | public Dictionary Meta { get; set; } = new(StringComparer.Ordinal); 44 | } 45 | 46 | [DeepComparable(CycleTracking = false)] 47 | public sealed class Customer 48 | { 49 | public Guid Id { get; set; } 50 | public string FullName { get; set; } = ""; 51 | public Region Region { get; set; } 52 | public Address ShipTo { get; set; } = new(); 53 | public List Orders { get; set; } = new(); 54 | } 55 | 56 | [DeepComparable(CycleTracking = false)] 57 | public sealed class MidGraph 58 | { 59 | public string Title { get; set; } = ""; 60 | public List Customers { get; set; } = new(); 61 | public Dictionary PriceIndex { get; set; } = new(StringComparer.Ordinal); 62 | public object? Polymorph { get; set; } 63 | public IDictionary Extra { get; set; } = new ExpandoObject(); 64 | } 65 | 66 | public static class MidGraphFactory 67 | { 68 | public static MidGraph Create(int customers = 40, int ordersPerCustomer = 3, int linesPerOrder = 4, int seed = 123) 69 | { 70 | var rng = new Random(seed); 71 | 72 | var g = new MidGraph 73 | { 74 | Title = $"MidGraph-{seed}", 75 | Polymorph = (seed % 2 == 0) 76 | ? (object)$"poly-{seed}" 77 | : (object)new Address { Line1 = "1 High St", City = "London", Postcode = "E1 1AA", Country = "UK" } 78 | }; 79 | 80 | for (int i = 0; i < 50; i++) 81 | g.PriceIndex[$"SKU-{i:D4}"] = 10 + (i % 7); 82 | 83 | var ex = (IDictionary)g.Extra; 84 | ex["build"] = seed; 85 | ex["flags"] = new[] { "x", "y", "z" }; 86 | ex["meta"] = new Dictionary(StringComparer.Ordinal) 87 | { 88 | ["channel"] = "web", 89 | ["ab"] = new[] { "A", "B" } 90 | }; 91 | 92 | for (int c = 0; c < customers; c++) 93 | { 94 | var cust = new Customer 95 | { 96 | Id = GuidFrom($"C{seed}-{c}"), 97 | FullName = $"Customer {c}", 98 | Region = (Region)(c % 3), 99 | ShipTo = new Address 100 | { 101 | Line1 = $"{c} Road", 102 | City = (c % 2 == 0) ? "London" : "Paris", 103 | Postcode = $"PC{c:000}", 104 | Country = (c % 2 == 0) ? "UK" : "FR" 105 | } 106 | }; 107 | 108 | for (int o = 0; o < ordersPerCustomer; o++) 109 | { 110 | var order = new Order 111 | { 112 | Id = GuidFrom($"C{c}-O{o}-{seed}"), 113 | Created = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero).AddMinutes(o) 114 | }; 115 | 116 | for (int l = 0; l < linesPerOrder; l++) 117 | { 118 | var sku = $"SKU-{(c + o + l) % 50:D4}"; 119 | var price = g.PriceIndex[sku]; 120 | order.Lines.Add(new OrderLine 121 | { 122 | Sku = sku, 123 | Qty = 1 + (l % 3), 124 | LineTotal = price * (1 + (l % 3)) 125 | }); 126 | } 127 | 128 | order.Meta["channel"] = (o % 2 == 0) ? "web" : "app"; 129 | order.Meta["bucket"] = ((c + o) % 5).ToString(); 130 | cust.Orders.Add(order); 131 | } 132 | 133 | g.Customers.Add(cust); 134 | } 135 | 136 | return g; 137 | 138 | static Guid GuidFrom(string s) 139 | { 140 | var bytes = System.Text.Encoding.UTF8.GetBytes(s); 141 | Span g = stackalloc byte[16]; 142 | for (int i = 0; i < 16; i++) g[i] = (byte)(bytes[i % bytes.Length] + i * 31); 143 | return new Guid(g); 144 | } 145 | } 146 | } 147 | 148 | public static class ManualNonLinq 149 | { 150 | public static bool AreEqual(MidGraph? a, MidGraph? b) 151 | { 152 | if (ReferenceEquals(a, b)) return true; 153 | if (a is null || b is null) return false; 154 | if (!string.Equals(a.Title, b.Title, StringComparison.Ordinal)) return false; 155 | 156 | if (!DictEqual(a.PriceIndex, b.PriceIndex)) return false; 157 | 158 | if (!ObjectEqual(a.Polymorph, b.Polymorph)) return false; 159 | if (!DynamicEqual(a.Extra, b.Extra)) return false; 160 | 161 | if (a.Customers.Count != b.Customers.Count) return false; 162 | for (int i = 0; i < a.Customers.Count; i++) 163 | if (!CustomerEqual(a.Customers[i], b.Customers[i])) return false; 164 | 165 | return true; 166 | } 167 | private static bool CustomerEqual(Customer? a, Customer? b) 168 | { 169 | if (ReferenceEquals(a, b)) return true; 170 | if (a is null || b is null) return false; 171 | if (!string.Equals(a.FullName, b.FullName, StringComparison.Ordinal)) return false; 172 | if (a.Region != b.Region) return false; 173 | if (!AddressEqual(a.ShipTo, b.ShipTo)) return false; 174 | 175 | if (a.Orders.Count != b.Orders.Count) return false; 176 | for (int i = 0; i < a.Orders.Count; i++) 177 | if (!OrderEqual(a.Orders[i], b.Orders[i])) return false; 178 | return true; 179 | } 180 | private static bool OrderEqual(Order? a, Order? b) 181 | { 182 | if (ReferenceEquals(a, b)) return true; 183 | if (a is null || b is null) return false; 184 | if (a.Id != b.Id) return false; 185 | if (a.Created != b.Created) return false; 186 | 187 | if (!DictEqual(a.Meta, b.Meta)) return false; 188 | 189 | if (a.Lines.Count != b.Lines.Count) return false; 190 | for (int i = 0; i < a.Lines.Count; i++) 191 | if (!LineEqual(a.Lines[i], b.Lines[i])) return false; 192 | return true; 193 | } 194 | private static bool LineEqual(OrderLine? a, OrderLine? b) 195 | { 196 | if (ReferenceEquals(a, b)) return true; 197 | if (a is null || b is null) return false; 198 | return a.Sku == b.Sku && a.Qty == b.Qty && a.LineTotal == b.LineTotal; 199 | } 200 | private static bool AddressEqual(Address? a, Address? b) 201 | { 202 | if (ReferenceEquals(a, b)) return true; 203 | if (a is null || b is null) return false; 204 | return a.Line1 == b.Line1 && a.City == b.City && a.Postcode == b.Postcode && a.Country == b.Country; 205 | } 206 | private static bool DictEqual(Dictionary? a, Dictionary? b) 207 | where TKey : notnull 208 | { 209 | if (ReferenceEquals(a, b)) return true; 210 | if (a is null || b is null) return false; 211 | if (a.Count != b.Count) return false; 212 | foreach (var kv in a) 213 | { 214 | if (!b.TryGetValue(kv.Key, out var bv)) return false; 215 | if (!Equals(kv.Value, bv)) return false; 216 | } 217 | return true; 218 | } 219 | private static bool ObjectEqual(object? a, object? b) 220 | { 221 | if (ReferenceEquals(a, b)) return true; 222 | if (a is null || b is null) return false; 223 | if (a.GetType() != b.GetType()) return false; 224 | if (a is string sa) return string.Equals(sa, (string)b, StringComparison.Ordinal); 225 | if (a is Address aa) return AddressEqual(aa, (Address)b); 226 | return a.Equals(b); 227 | } 228 | private static bool DynamicEqual(IDictionary a, IDictionary b) 229 | { 230 | if (a.Count != b.Count) return false; 231 | foreach (var kv in a) 232 | { 233 | if (!b.TryGetValue(kv.Key, out var bv)) return false; 234 | if (!ObjectEqual(kv.Value, bv)) return false; 235 | } 236 | return true; 237 | } 238 | } 239 | 240 | public static class ManualLinqy 241 | { 242 | public static bool AreEqual(MidGraph? a, MidGraph? b) 243 | { 244 | if (ReferenceEquals(a, b)) return true; 245 | if (a is null || b is null) return false; 246 | 247 | return a.Title == b.Title 248 | && DictEqual(a.PriceIndex, b.PriceIndex) 249 | && ObjectEqual(a.Polymorph, b.Polymorph) 250 | && DynamicEqual(a.Extra, b.Extra) 251 | && a.Customers.SequenceEqual(b.Customers, new CustomerEq()); 252 | } 253 | 254 | private sealed class CustomerEq : IEqualityComparer 255 | { 256 | public bool Equals(Customer? x, Customer? y) 257 | { 258 | if (ReferenceEquals(x, y)) return true; 259 | if (x is null || y is null) return false; 260 | return x.FullName == y.FullName 261 | && x.Region == y.Region 262 | && AddressEqual(x.ShipTo, y.ShipTo) 263 | && x.Orders.SequenceEqual(y.Orders, new OrderEq()); 264 | } 265 | public int GetHashCode(Customer obj) => 0; 266 | } 267 | 268 | private sealed class OrderEq : IEqualityComparer 269 | { 270 | public bool Equals(Order? x, Order? y) 271 | { 272 | if (ReferenceEquals(x, y)) return true; 273 | if (x is null || y is null) return false; 274 | return x.Id == y.Id 275 | && x.Created.Equals(y.Created) 276 | && DictEqual(x.Meta, y.Meta) 277 | && x.Lines.SequenceEqual(y.Lines, new LineEq()); 278 | } 279 | public int GetHashCode(Order obj) => 0; 280 | } 281 | 282 | private sealed class LineEq : IEqualityComparer 283 | { 284 | public bool Equals(OrderLine? x, OrderLine? y) 285 | => x!.Sku == y!.Sku && x.Qty == y.Qty && x.LineTotal == y.LineTotal; 286 | public int GetHashCode(OrderLine obj) => 0; 287 | } 288 | 289 | private static bool AddressEqual(Address? a, Address? b) 290 | { 291 | if (ReferenceEquals(a, b)) return true; 292 | if (a is null || b is null) return false; 293 | return a.Line1 == b.Line1 && a.City == b.City && a.Postcode == b.Postcode && a.Country == b.Country; 294 | } 295 | private static bool DictEqual(Dictionary a, Dictionary b) 296 | where TKey : notnull 297 | => a.Count == b.Count && a.All(kv => b.TryGetValue(kv.Key, out var bv) && Equals(kv.Value, bv)); 298 | private static bool ObjectEqual(object? a, object? b) 299 | { 300 | if (ReferenceEquals(a, b)) return true; 301 | if (a is null || b is null) return false; 302 | if (a.GetType() != b.GetType()) return false; 303 | return a switch 304 | { 305 | string sa => sa == (string)b!, 306 | Address aa => AddressEqual(aa, (Address)b!), 307 | _ => a.Equals(b) 308 | }; 309 | } 310 | private static bool DynamicEqual(IDictionary a, IDictionary b) 311 | => a.Count == b.Count && a.All(kv => b.TryGetValue(kv.Key, out var bv) && ObjectEqual(kv.Value, bv)); 312 | } 313 | 314 | [MemoryDiagnoser] 315 | [PlainExporter] 316 | public class MidGraphBenchmarks 317 | { 318 | [Params(40)] public int Customers; 319 | [Params(3)] public int OrdersPerCustomer; 320 | [Params(4)] public int LinesPerOrder; 321 | 322 | private MidGraph _eqA = null!; 323 | private MidGraph _eqB = null!; 324 | private MidGraph _neqShallowA = null!; 325 | private MidGraph _neqShallowB = null!; 326 | private MidGraph _neqDeepA = null!; 327 | private MidGraph _neqDeepB = null!; 328 | 329 | private CompareLogic _cno = null!; 330 | private ObjectsComparer.Comparer _objComp = null!; 331 | 332 | [GlobalSetup] 333 | public void Setup() 334 | { 335 | _eqA = MidGraphFactory.Create(Customers, OrdersPerCustomer, LinesPerOrder, seed: 11); 336 | _eqB = MidGraphFactory.Create(Customers, OrdersPerCustomer, LinesPerOrder, seed: 11); 337 | 338 | _neqShallowA = MidGraphFactory.Create(Customers, OrdersPerCustomer, LinesPerOrder, seed: 22); 339 | _neqShallowB = MidGraphFactory.Create(Customers, OrdersPerCustomer, LinesPerOrder, seed: 22); 340 | _neqShallowB.Title += "-DIFF"; 341 | 342 | _neqDeepA = MidGraphFactory.Create(Customers, OrdersPerCustomer, LinesPerOrder, seed: 33); 343 | _neqDeepB = MidGraphFactory.Create(Customers, OrdersPerCustomer, LinesPerOrder, seed: 33); 344 | var lastC = _neqDeepB.Customers[^1]; 345 | var lastO = lastC.Orders[^1]; 346 | lastO.Lines[^1].Qty += 1; 347 | 348 | _cno = new CompareLogic(new ComparisonConfig 349 | { 350 | MaxDifferences = 1, 351 | Caching = true, 352 | IgnoreObjectTypes = false 353 | }); 354 | 355 | _objComp = new ObjectsComparer.Comparer(); 356 | } 357 | 358 | 359 | [Benchmark(Baseline = true)] 360 | public bool Generated_Equal() => MidGraphDeepEqual.AreDeepEqual(_eqA, _eqB); 361 | 362 | [Benchmark] public bool Manual_NonLinq_Equal() => ManualNonLinq.AreEqual(_eqA, _eqB); 363 | [Benchmark] public bool Manual_Linqy_Equal() => ManualLinqy.AreEqual(_eqA, _eqB); 364 | 365 | [Benchmark] 366 | public bool Json_Newtonsoft_Equal() 367 | { 368 | var ja = JToken.FromObject(_eqA); 369 | var jb = JToken.FromObject(_eqB); 370 | return JToken.DeepEquals(ja, jb); 371 | } 372 | 373 | [Benchmark] 374 | public bool Json_SystemText_Equal() 375 | { 376 | var sa = JsonSerializer.Serialize(_eqA); 377 | var sb = JsonSerializer.Serialize(_eqB); 378 | var na = JsonNode.Parse(sa)!; 379 | var nb = JsonNode.Parse(sb)!; 380 | return JsonNode.DeepEquals(na, nb); 381 | } 382 | 383 | [Benchmark] 384 | public bool CompareNetObjects_Equal() 385 | => _cno.Compare(_eqA, _eqB).AreEqual; 386 | 387 | [Benchmark] 388 | public bool ObjectsComparer_Equal() 389 | => _objComp.Compare(_eqA, _eqB); 390 | 391 | [Benchmark] 392 | public bool FluentAssertions_Equal() 393 | { 394 | try { _eqA.Should().BeEquivalentTo(_eqB); return true; } 395 | catch { return false; } 396 | } 397 | 398 | 399 | [Benchmark] public bool Generated_NotEqual_Shallow() => MidGraphDeepEqual.AreDeepEqual(_neqShallowA, _neqShallowB); 400 | [Benchmark] public bool Manual_NonLinq_NotEqual_Shallow() => ManualNonLinq.AreEqual(_neqShallowA, _neqShallowB); 401 | [Benchmark] public bool Manual_Linqy_NotEqual_Shallow() => ManualLinqy.AreEqual(_neqShallowA, _neqShallowB); 402 | 403 | [Benchmark] 404 | public bool Json_Newtonsoft_NotEqual_Shallow() 405 | { 406 | var ja = JToken.FromObject(_neqShallowA); 407 | var jb = JToken.FromObject(_neqShallowB); 408 | return JToken.DeepEquals(ja, jb); 409 | } 410 | 411 | [Benchmark] 412 | public bool Json_SystemText_NotEqual_Shallow() 413 | { 414 | var sa = JsonSerializer.Serialize(_neqShallowA); 415 | var sb = JsonSerializer.Serialize(_neqShallowB); 416 | var na = JsonNode.Parse(sa)!; 417 | var nb = JsonNode.Parse(sb)!; 418 | return JsonNode.DeepEquals(na, nb); 419 | } 420 | 421 | [Benchmark] 422 | public bool CompareNetObjects_NotEqual_Shallow() 423 | => _cno.Compare(_neqShallowA, _neqShallowB).AreEqual; 424 | 425 | [Benchmark] 426 | public bool ObjectsComparer_NotEqual_Shallow() 427 | => _objComp.Compare(_neqShallowA, _neqShallowB); 428 | 429 | [Benchmark] 430 | public bool FluentAssertions_NotEqual_Shallow() 431 | { 432 | try { _neqShallowA.Should().BeEquivalentTo(_neqShallowB); return true; } 433 | catch { return false; } 434 | } 435 | 436 | 437 | [Benchmark] public bool Generated_NotEqual_Deep() => MidGraphDeepEqual.AreDeepEqual(_neqDeepA, _neqDeepB); 438 | [Benchmark] public bool Manual_NonLinq_NotEqual_Deep() => ManualNonLinq.AreEqual(_neqDeepA, _neqDeepB); 439 | [Benchmark] public bool Manual_Linqy_NotEqual_Deep() => ManualLinqy.AreEqual(_neqDeepA, _neqDeepB); 440 | 441 | [Benchmark] 442 | public bool Json_Newtonsoft_NotEqual_Deep() 443 | { 444 | var ja = JToken.FromObject(_neqDeepA); 445 | var jb = JToken.FromObject(_neqDeepB); 446 | return JToken.DeepEquals(ja, jb); 447 | } 448 | 449 | [Benchmark] 450 | public bool Json_SystemText_NotEqual_Deep() 451 | { 452 | var sa = JsonSerializer.Serialize(_neqDeepA); 453 | var sb = JsonSerializer.Serialize(_neqDeepB); 454 | var na = JsonNode.Parse(sa)!; 455 | var nb = JsonNode.Parse(sb)!; 456 | return JsonNode.DeepEquals(na, nb); 457 | } 458 | 459 | [Benchmark] 460 | public bool CompareNetObjects_NotEqual_Deep() 461 | => _cno.Compare(_neqDeepA, _neqDeepB).AreEqual; 462 | 463 | [Benchmark] 464 | public bool ObjectsComparer_NotEqual_Deep() 465 | => _objComp.Compare(_neqDeepA, _neqDeepB); 466 | 467 | [Benchmark] 468 | public bool FluentAssertions_NotEqual_Deep() 469 | { 470 | try { _neqDeepA.Should().BeEquivalentTo(_neqDeepB); return true; } 471 | catch { return false; } 472 | } 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # DeepEqual.Generator 2 | 3 | A C# source generator that creates **super-fast, allocation-free deep equality comparers** for your classes and structs. 4 | 5 | Stop writing `Equals` by hand. Stop serializing to JSON just to compare objects. 6 | Just add an attribute, and you get a complete deep comparer generated at compile time. 7 | 8 | --- 9 | 10 | ## ✨ Why use this? 11 | 12 | * **Simple** – annotate your models, and you’re done. 13 | * **Flexible** – opt-in options for unordered collections, numeric tolerances, string case sensitivity, custom comparers. 14 | * **Robust** – covers tricky cases (cycles, sets, dictionaries, polymorphism) that manual code often misses. 15 | 16 | --- 17 | 18 | ## ⚡ Why is it faster than handwritten code? 19 | 20 | * **Compile-time codegen**: emitted at build time as optimized IL — no reflection, no runtime expression building. 21 | * **Direct member access**: expands equality checks into straight-line code instead of generic loops or helper calls. 22 | * **No allocations**: avoids closures, iterators, or boxing that sneak into LINQ or naive implementations. 23 | 24 | Result: consistently **5–7× faster** than handwritten comparers, and orders of magnitude faster than JSON/library approaches. 25 | 26 | --- 27 | 28 | ## 🛡️ Why is it more robust? 29 | 30 | * **Covers corner cases**: nested collections, dictionaries, sets, polymorphism, reference cycles. 31 | * **Deterministic**: guarantees the same behavior regardless of field order or shape. 32 | * **Safer than manual**: no risk of forgetting a property or comparing the wrong shape. 33 | 34 | In short: you get **the speed of hand-tuned code**, but with **the coverage of a well-tested library** — and without runtime overhead. 35 | 36 | --- 37 | 38 | ## 📦 Installation 39 | 40 | You need **two packages**: 41 | 42 | ```powershell 43 | dotnet add package DeepEqual.Generator.Shared 44 | dotnet add package DeepEqual.Generator 45 | ``` 46 | 47 | * **Shared** → contains runtime comparers and attributes. 48 | * **Generator** → analyzer that emits the equality code at compile time. 49 | 50 | If you install only the generator, builds will fail because the generated code depends on the runtime package. 51 | 52 | --- 53 | 54 | ## 🚀 Quick start 55 | 56 | Annotate your type: 57 | 58 | ```csharp 59 | using DeepEqual.Generator.Shared; 60 | 61 | [DeepComparable] 62 | public sealed class Person 63 | { 64 | public string Name { get; set; } = ""; 65 | public int Age { get; set; } 66 | } 67 | ``` 68 | 69 | At compile time, a static helper is generated: 70 | 71 | ```csharp 72 | PersonDeepEqual.AreDeepEqual(personA, personB); 73 | ``` 74 | 75 | --- 76 | 77 | ## 🔍 Supported comparisons 78 | 79 | * **Primitives & enums** – by value. 80 | * **Strings** – configurable (ordinal, ignore case, culture aware). 81 | * **DateTime / DateTimeOffset** – strict (both `Kind`/`Offset` and `Ticks` must match). 82 | * **Guid, TimeSpan, DateOnly, TimeOnly** – by value. 83 | * **Nullable** – compared only if both have a value. 84 | * **Arrays & collections** – element by element. 85 | * **Dictionaries** – key/value pairs deeply compared. 86 | * **Jagged & multidimensional arrays** – handled correctly. 87 | * **Object** properties – compared polymorphically if the runtime type has a generated helper. 88 | * **Dynamics / ExpandoObject** – compared as dictionaries. 89 | * **Cycles** – supported (can be turned off if you know your graph has no cycles). 90 | 91 | --- 92 | 93 | ## 🎛 Options 94 | 95 | ### On the root type 96 | 97 | ```csharp 98 | [DeepComparable(OrderInsensitiveCollections = true, IncludeInternals = true, IncludeBaseMembers = true)] 99 | public sealed class Order { … } 100 | ``` 101 | 102 | **Defaults:** 103 | 104 | * `OrderInsensitiveCollections` → **false** 105 | * `IncludeInternals` → **false** 106 | * `IncludeBaseMembers` → **true** 107 | * `CycleTracking` → **true** 108 | 109 | ### On individual members 110 | 111 | ```csharp 112 | public sealed class Person 113 | { 114 | [DeepCompare(Kind = CompareKind.Shallow)] 115 | public Address? Home { get; set; } 116 | 117 | [DeepCompare(OrderInsensitive = true)] 118 | public List? Tags { get; set; } 119 | 120 | [DeepCompare(IgnoreMembers = new[] { "CreatedAt", "UpdatedAt" })] 121 | public AuditInfo Info { get; set; } = new(); 122 | } 123 | ``` 124 | 125 | --- 126 | 127 | ## 📚 Ordered vs unordered collections 128 | 129 | By default, collections are compared **in order**. If you want them compared ignoring order (like sets), you can: 130 | 131 | * Enable globally: 132 | 133 | ```csharp 134 | [DeepComparable(OrderInsensitiveCollections = true)] 135 | public sealed class OrderBatch 136 | { 137 | public List Ids { get; set; } = new(); 138 | } 139 | ``` 140 | 141 | * On specific members: 142 | 143 | ```csharp 144 | public sealed class TagSet 145 | { 146 | [DeepCompare(OrderInsensitive = true)] 147 | public List Tags { get; set; } = new(); 148 | } 149 | ``` 150 | 151 | * Or use **key-based matching**: 152 | 153 | ```csharp 154 | [DeepCompare(KeyMembers = new[] { "Id" })] 155 | public sealed class Customer 156 | { 157 | public string Id { get; set; } = ""; 158 | public string Name { get; set; } = ""; 159 | } 160 | ``` 161 | 162 | --- 163 | 164 | ## ⚡ Numeric & string options 165 | 166 | ```csharp 167 | var opts = new ComparisonOptions 168 | { 169 | FloatEpsilon = 0f, 170 | DoubleEpsilon = 0d, 171 | DecimalEpsilon = 0m, 172 | TreatNaNEqual = false, 173 | StringComparison = StringComparison.Ordinal 174 | }; 175 | ``` 176 | 177 | Defaults: strict equality for numbers and case-sensitive ordinal for strings. 178 | 179 | --- 180 | 181 | ## 🌀 Cycles 182 | 183 | Cyclic graphs are handled safely: 184 | 185 | ```csharp 186 | [DeepComparable] 187 | public sealed class Node 188 | { 189 | public string Id { get; set; } = ""; 190 | public Node? Next { get; set; } 191 | } 192 | 193 | var a = new Node { Id = "a" }; 194 | var b = new Node { Id = "a" }; 195 | a.Next = a; 196 | b.Next = b; 197 | 198 | NodeDeepEqual.AreDeepEqual(a, b); 199 | ``` 200 | 201 | --- 202 | 203 | ## 📊 Benchmarks 204 | 205 | The generated comparer outperforms handwritten, JSON, and popular libraries by a wide margin: 206 | 207 | | Method | Equal | Allocations | 208 | | ------------------- | --------: | ----------: | 209 | | **Generated** | 0.3 µs | 120 B | 210 | | Handwritten (Linq) | 2.1 µs | 3.5 KB | 211 | | JSON (STJ) | 1.401 ms | 1.4 MB | 212 | | Compare-Net-Objects | 2.099 ms | 3.4 MB | 213 | | ObjectsComparer | 13.527 ms | 13 MB | 214 | | FluentAssertions | 10.818 ms | 21 MB | 215 | 216 | --- 217 | 218 | ## ✅ When to use 219 | 220 | * Large object graphs (domain models, caches, trees). 221 | * Unit/integration tests where you assert deep equality. 222 | * Regression testing with snapshot objects. 223 | * High-throughput APIs needing object deduplication. 224 | * Anywhere you need correctness *and* speed. 225 | 226 | --- 227 | 228 | ## 📦 Roadmap 229 | 230 | * [x] Strict time semantics 231 | * [x] Numeric tolerances 232 | * [x] String comparison options 233 | * [x] Cycle tracking 234 | * [x] Include internals & base members 235 | * [x] Order-insensitive collections 236 | * [x] Key-based unordered matching 237 | * [x] Custom comparers 238 | * [x] Memory / ReadOnlyMemory 239 | * [x] Benchmarks & tests 240 | * [ ] Analyzer diagnostics 241 | * [ ] Developer guide & samples site 242 | 243 | 244 | ## 🔍 Generated Code Example 245 | 246 | Given the graph: 247 | 248 | ``` 249 | public enum Region { NA, EU, APAC } 250 | 251 | [DeepComparable(CycleTracking = false)] 252 | public sealed class Address 253 | { 254 | public string Line1 { get; set; } = ""; 255 | public string City { get; set; } = ""; 256 | public string Postcode { get; set; } = ""; 257 | public string Country { get; set; } = ""; 258 | public ExpandoObject Countr3y { get; set; } 259 | } 260 | 261 | [DeepComparable(CycleTracking = false)] 262 | public sealed class OrderLine 263 | { 264 | public string Sku { get; set; } = ""; 265 | public int Qty { get; set; } 266 | public decimal LineTotal { get; set; } 267 | } 268 | 269 | [DeepComparable(CycleTracking = false)] 270 | public sealed class Order 271 | { 272 | public Guid Id { get; set; } 273 | public DateTimeOffset Created { get; set; } 274 | public List Lines { get; set; } = new(); 275 | public Dictionary Meta { get; set; } = new(StringComparer.Ordinal); 276 | } 277 | 278 | [DeepComparable(CycleTracking = false)] 279 | public sealed class Customer 280 | { 281 | public Guid Id { get; set; } 282 | public string FullName { get; set; } = ""; 283 | public Region Region { get; set; } 284 | public Address ShipTo { get; set; } = new(); 285 | public List Orders { get; set; } = new(); 286 | } 287 | 288 | [DeepComparable(CycleTracking = false)] 289 | public sealed class MidGraph 290 | { 291 | public string Title { get; set; } = ""; 292 | public List Customers { get; set; } = new(); 293 | public Dictionary PriceIndex { get; set; } = new(StringComparer.Ordinal); 294 | public object? Polymorph { get; set; } 295 | public IDictionary Extra { get; set; } = new ExpandoObject(); 296 | } 297 | ``` 298 | 299 | The generated code is: 300 | 301 | ``` 302 | // 303 | #pragma warning disable 304 | using System; 305 | using System.Collections; 306 | using System.Collections.Generic; 307 | using DeepEqual.Generator.Shared; 308 | 309 | namespace DeepEqual.Generator.Benchmarking 310 | { 311 | public static class MidGraphDeepEqual 312 | { 313 | static MidGraphDeepEqual() 314 | { 315 | GeneratedHelperRegistry.Register((l, r, c) => AreDeepEqual__global__DeepEqual_Generator_Benchmarking_Address(l, r, c)); 316 | GeneratedHelperRegistry.Register((l, r, c) => AreDeepEqual__global__DeepEqual_Generator_Benchmarking_Customer(l, r, c)); 317 | GeneratedHelperRegistry.Register((l, r, c) => AreDeepEqual__global__DeepEqual_Generator_Benchmarking_MidGraph(l, r, c)); 318 | GeneratedHelperRegistry.Register((l, r, c) => AreDeepEqual__global__DeepEqual_Generator_Benchmarking_Order(l, r, c)); 319 | GeneratedHelperRegistry.Register((l, r, c) => AreDeepEqual__global__DeepEqual_Generator_Benchmarking_OrderLine(l, r, c)); 320 | } 321 | 322 | public static bool AreDeepEqual(global::DeepEqual.Generator.Benchmarking.MidGraph? left, global::DeepEqual.Generator.Benchmarking.MidGraph? right) 323 | { 324 | if (object.ReferenceEquals(left, right)) 325 | { 326 | return true; 327 | } 328 | if (left is null || right is null) 329 | { 330 | return false; 331 | } 332 | var context = DeepEqual.Generator.Shared.ComparisonContext.NoTracking; 333 | return AreDeepEqual__global__DeepEqual_Generator_Benchmarking_MidGraph(left, right, context); 334 | } 335 | 336 | public static bool AreDeepEqual(global::DeepEqual.Generator.Benchmarking.MidGraph? left, global::DeepEqual.Generator.Benchmarking.MidGraph? right, DeepEqual.Generator.Shared.ComparisonOptions options) 337 | { 338 | if (object.ReferenceEquals(left, right)) 339 | { 340 | return true; 341 | } 342 | if (left is null || right is null) 343 | { 344 | return false; 345 | } 346 | var context = new DeepEqual.Generator.Shared.ComparisonContext(options); 347 | return AreDeepEqual__global__DeepEqual_Generator_Benchmarking_MidGraph(left, right, context); 348 | } 349 | 350 | public static bool AreDeepEqual(global::DeepEqual.Generator.Benchmarking.MidGraph? left, global::DeepEqual.Generator.Benchmarking.MidGraph? right, DeepEqual.Generator.Shared.ComparisonContext context) 351 | { 352 | if (object.ReferenceEquals(left, right)) 353 | { 354 | return true; 355 | } 356 | if (left is null || right is null) 357 | { 358 | return false; 359 | } 360 | return AreDeepEqual__global__DeepEqual_Generator_Benchmarking_MidGraph(left, right, context); 361 | } 362 | 363 | private static bool AreDeepEqual__global__DeepEqual_Generator_Benchmarking_Address(global::DeepEqual.Generator.Benchmarking.Address left, global::DeepEqual.Generator.Benchmarking.Address right, DeepEqual.Generator.Shared.ComparisonContext context) 364 | { 365 | if (object.ReferenceEquals(left, right)) 366 | { 367 | return true; 368 | } 369 | if (left is null || right is null) 370 | { 371 | return false; 372 | } 373 | if (!object.ReferenceEquals(left.City, right.City)) 374 | { 375 | if (left.City is null || right.City is null) 376 | { 377 | return false; 378 | } 379 | } 380 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(left.City, right.City, context)) 381 | { 382 | return false; 383 | } 384 | 385 | if (!object.ReferenceEquals(left.Country, right.Country)) 386 | { 387 | if (left.Country is null || right.Country is null) 388 | { 389 | return false; 390 | } 391 | } 392 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(left.Country, right.Country, context)) 393 | { 394 | return false; 395 | } 396 | 397 | if (!object.ReferenceEquals(left.Line1, right.Line1)) 398 | { 399 | if (left.Line1 is null || right.Line1 is null) 400 | { 401 | return false; 402 | } 403 | } 404 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(left.Line1, right.Line1, context)) 405 | { 406 | return false; 407 | } 408 | 409 | if (!object.ReferenceEquals(left.Postcode, right.Postcode)) 410 | { 411 | if (left.Postcode is null || right.Postcode is null) 412 | { 413 | return false; 414 | } 415 | } 416 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(left.Postcode, right.Postcode, context)) 417 | { 418 | return false; 419 | } 420 | 421 | return true; 422 | } 423 | 424 | private static bool AreDeepEqual__global__DeepEqual_Generator_Benchmarking_Customer(global::DeepEqual.Generator.Benchmarking.Customer left, global::DeepEqual.Generator.Benchmarking.Customer right, DeepEqual.Generator.Shared.ComparisonContext context) 425 | { 426 | if (object.ReferenceEquals(left, right)) 427 | { 428 | return true; 429 | } 430 | if (left is null || right is null) 431 | { 432 | return false; 433 | } 434 | if (!object.ReferenceEquals(left.FullName, right.FullName)) 435 | { 436 | if (left.FullName is null || right.FullName is null) 437 | { 438 | return false; 439 | } 440 | } 441 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(left.FullName, right.FullName, context)) 442 | { 443 | return false; 444 | } 445 | 446 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualEnum(left.Region, right.Region)) 447 | { 448 | return false; 449 | } 450 | 451 | if (!left.Id.Equals(right.Id)) 452 | { 453 | return false; 454 | } 455 | 456 | if (!object.ReferenceEquals(left.ShipTo, right.ShipTo)) 457 | { 458 | if (left.ShipTo is null || right.ShipTo is null) 459 | { 460 | return false; 461 | } 462 | } 463 | if (!(DeepEqual.Generator.Shared.ComparisonHelpers.DeepComparePolymorphic(left.ShipTo, right.ShipTo, context))) 464 | { 465 | return false; 466 | } 467 | 468 | if (!object.ReferenceEquals(left.Orders, right.Orders)) 469 | { 470 | if (left.Orders is null || right.Orders is null) 471 | { 472 | return false; 473 | } 474 | } 475 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualSequencesOrdered(left.Orders as IEnumerable, right.Orders as IEnumerable, new __Cmp__global__DeepEqual_Generator_Benchmarking_Order__M_global__DeepEqual_Generator_Benchmarking_Customer_Orders(), context)) 476 | { 477 | return false; 478 | } 479 | 480 | return true; 481 | } 482 | 483 | private static bool AreDeepEqual__global__DeepEqual_Generator_Benchmarking_MidGraph(global::DeepEqual.Generator.Benchmarking.MidGraph left, global::DeepEqual.Generator.Benchmarking.MidGraph right, DeepEqual.Generator.Shared.ComparisonContext context) 484 | { 485 | if (object.ReferenceEquals(left, right)) 486 | { 487 | return true; 488 | } 489 | if (left is null || right is null) 490 | { 491 | return false; 492 | } 493 | if (!object.ReferenceEquals(left.Title, right.Title)) 494 | { 495 | if (left.Title is null || right.Title is null) 496 | { 497 | return false; 498 | } 499 | } 500 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(left.Title, right.Title, context)) 501 | { 502 | return false; 503 | } 504 | 505 | if (!object.ReferenceEquals(left.Polymorph, right.Polymorph)) 506 | { 507 | if (left.Polymorph is null || right.Polymorph is null) 508 | { 509 | return false; 510 | } 511 | } 512 | if (!DeepEqual.Generator.Shared.DynamicDeepComparer.AreEqualDynamic(left.Polymorph, right.Polymorph, context)) 513 | { 514 | return false; 515 | } 516 | 517 | if (!object.ReferenceEquals(left.Extra, right.Extra)) 518 | { 519 | if (left.Extra is null || right.Extra is null) 520 | { 521 | return false; 522 | } 523 | } 524 | var __roMapA_MidGraph_Extra = left.Extra as global::System.Collections.Generic.IDictionary; 525 | var __roMapB_MidGraph_Extra = right.Extra as global::System.Collections.Generic.IDictionary; 526 | if (__roMapA_MidGraph_Extra is not null && __roMapB_MidGraph_Extra is not null) 527 | { 528 | if (__roMapA_MidGraph_Extra.Count != __roMapB_MidGraph_Extra.Count) 529 | { 530 | return false; 531 | } 532 | foreach (var __kv in __roMapA_MidGraph_Extra) 533 | { 534 | if (!__roMapB_MidGraph_Extra.TryGetValue(__kv.Key, out var __rv)) 535 | { 536 | return false; 537 | } 538 | if (!(DeepEqual.Generator.Shared.DynamicDeepComparer.AreEqualDynamic(__kv.Value, __rv, context))) 539 | { 540 | return false; 541 | } 542 | } 543 | return true; 544 | } 545 | var __rwMapA_MidGraph_Extra = left.Extra as global::System.Collections.Generic.IDictionary; 546 | var __rwMapB_MidGraph_Extra = right.Extra as global::System.Collections.Generic.IDictionary; 547 | if (__rwMapA_MidGraph_Extra is not null && __rwMapB_MidGraph_Extra is not null) 548 | { 549 | if (__rwMapA_MidGraph_Extra.Count != __rwMapB_MidGraph_Extra.Count) 550 | { 551 | return false; 552 | } 553 | foreach (var __kv in __rwMapA_MidGraph_Extra) 554 | { 555 | if (!__rwMapB_MidGraph_Extra.TryGetValue(__kv.Key, out var __rv)) 556 | { 557 | return false; 558 | } 559 | if (!(DeepEqual.Generator.Shared.DynamicDeepComparer.AreEqualDynamic(__kv.Value, __rv, context))) 560 | { 561 | return false; 562 | } 563 | } 564 | return true; 565 | } 566 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualDictionariesAny(left.Extra, right.Extra, new __Cmp__object__M_global__DeepEqual_Generator_Benchmarking_MidGraph_Extra_Val(), context)) 567 | { 568 | return false; 569 | } 570 | 571 | if (!object.ReferenceEquals(left.PriceIndex, right.PriceIndex)) 572 | { 573 | if (left.PriceIndex is null || right.PriceIndex is null) 574 | { 575 | return false; 576 | } 577 | } 578 | var __roMapA_MidGraph_PriceIndex = left.PriceIndex as global::System.Collections.Generic.IDictionary; 579 | var __roMapB_MidGraph_PriceIndex = right.PriceIndex as global::System.Collections.Generic.IDictionary; 580 | if (__roMapA_MidGraph_PriceIndex is not null && __roMapB_MidGraph_PriceIndex is not null) 581 | { 582 | if (__roMapA_MidGraph_PriceIndex.Count != __roMapB_MidGraph_PriceIndex.Count) 583 | { 584 | return false; 585 | } 586 | foreach (var __kv in __roMapA_MidGraph_PriceIndex) 587 | { 588 | if (!__roMapB_MidGraph_PriceIndex.TryGetValue(__kv.Key, out var __rv)) 589 | { 590 | return false; 591 | } 592 | if (!(DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualDecimal(__kv.Value, __rv, context))) 593 | { 594 | return false; 595 | } 596 | } 597 | return true; 598 | } 599 | var __rwMapA_MidGraph_PriceIndex = left.PriceIndex as global::System.Collections.Generic.IDictionary; 600 | var __rwMapB_MidGraph_PriceIndex = right.PriceIndex as global::System.Collections.Generic.IDictionary; 601 | if (__rwMapA_MidGraph_PriceIndex is not null && __rwMapB_MidGraph_PriceIndex is not null) 602 | { 603 | if (__rwMapA_MidGraph_PriceIndex.Count != __rwMapB_MidGraph_PriceIndex.Count) 604 | { 605 | return false; 606 | } 607 | foreach (var __kv in __rwMapA_MidGraph_PriceIndex) 608 | { 609 | if (!__rwMapB_MidGraph_PriceIndex.TryGetValue(__kv.Key, out var __rv)) 610 | { 611 | return false; 612 | } 613 | if (!(DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualDecimal(__kv.Value, __rv, context))) 614 | { 615 | return false; 616 | } 617 | } 618 | return true; 619 | } 620 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualDictionariesAny(left.PriceIndex, right.PriceIndex, new __Cmp__decimal__M_global__DeepEqual_Generator_Benchmarking_MidGraph_PriceIndex_Val(), context)) 621 | { 622 | return false; 623 | } 624 | 625 | if (!object.ReferenceEquals(left.Customers, right.Customers)) 626 | { 627 | if (left.Customers is null || right.Customers is null) 628 | { 629 | return false; 630 | } 631 | } 632 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualSequencesOrdered(left.Customers as IEnumerable, right.Customers as IEnumerable, new __Cmp__global__DeepEqual_Generator_Benchmarking_Customer__M_global__DeepEqual_Generator_Benchmarking_MidGraph_Customers(), context)) 633 | { 634 | return false; 635 | } 636 | 637 | return true; 638 | } 639 | 640 | private static bool AreDeepEqual__global__DeepEqual_Generator_Benchmarking_Order(global::DeepEqual.Generator.Benchmarking.Order left, global::DeepEqual.Generator.Benchmarking.Order right, DeepEqual.Generator.Shared.ComparisonContext context) 641 | { 642 | if (object.ReferenceEquals(left, right)) 643 | { 644 | return true; 645 | } 646 | if (left is null || right is null) 647 | { 648 | return false; 649 | } 650 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualDateTimeOffset(left.Created, right.Created)) 651 | { 652 | return false; 653 | } 654 | 655 | if (!left.Id.Equals(right.Id)) 656 | { 657 | return false; 658 | } 659 | 660 | if (!object.ReferenceEquals(left.Meta, right.Meta)) 661 | { 662 | if (left.Meta is null || right.Meta is null) 663 | { 664 | return false; 665 | } 666 | } 667 | var __roMapA_Order_Meta = left.Meta as global::System.Collections.Generic.IDictionary; 668 | var __roMapB_Order_Meta = right.Meta as global::System.Collections.Generic.IDictionary; 669 | if (__roMapA_Order_Meta is not null && __roMapB_Order_Meta is not null) 670 | { 671 | if (__roMapA_Order_Meta.Count != __roMapB_Order_Meta.Count) 672 | { 673 | return false; 674 | } 675 | foreach (var __kv in __roMapA_Order_Meta) 676 | { 677 | if (!__roMapB_Order_Meta.TryGetValue(__kv.Key, out var __rv)) 678 | { 679 | return false; 680 | } 681 | if (!(DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(__kv.Value, __rv, context))) 682 | { 683 | return false; 684 | } 685 | } 686 | return true; 687 | } 688 | var __rwMapA_Order_Meta = left.Meta as global::System.Collections.Generic.IDictionary; 689 | var __rwMapB_Order_Meta = right.Meta as global::System.Collections.Generic.IDictionary; 690 | if (__rwMapA_Order_Meta is not null && __rwMapB_Order_Meta is not null) 691 | { 692 | if (__rwMapA_Order_Meta.Count != __rwMapB_Order_Meta.Count) 693 | { 694 | return false; 695 | } 696 | foreach (var __kv in __rwMapA_Order_Meta) 697 | { 698 | if (!__rwMapB_Order_Meta.TryGetValue(__kv.Key, out var __rv)) 699 | { 700 | return false; 701 | } 702 | if (!(DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(__kv.Value, __rv, context))) 703 | { 704 | return false; 705 | } 706 | } 707 | return true; 708 | } 709 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualDictionariesAny(left.Meta, right.Meta, new __Cmp__string__M_global__DeepEqual_Generator_Benchmarking_Order_Meta_Val(), context)) 710 | { 711 | return false; 712 | } 713 | 714 | if (!object.ReferenceEquals(left.Lines, right.Lines)) 715 | { 716 | if (left.Lines is null || right.Lines is null) 717 | { 718 | return false; 719 | } 720 | } 721 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualSequencesOrdered(left.Lines as IEnumerable, right.Lines as IEnumerable, new __Cmp__global__DeepEqual_Generator_Benchmarking_OrderLine__M_global__DeepEqual_Generator_Benchmarking_Order_Lines(), context)) 722 | { 723 | return false; 724 | } 725 | 726 | return true; 727 | } 728 | 729 | private static bool AreDeepEqual__global__DeepEqual_Generator_Benchmarking_OrderLine(global::DeepEqual.Generator.Benchmarking.OrderLine left, global::DeepEqual.Generator.Benchmarking.OrderLine right, DeepEqual.Generator.Shared.ComparisonContext context) 730 | { 731 | if (object.ReferenceEquals(left, right)) 732 | { 733 | return true; 734 | } 735 | if (left is null || right is null) 736 | { 737 | return false; 738 | } 739 | if (!object.ReferenceEquals(left.Sku, right.Sku)) 740 | { 741 | if (left.Sku is null || right.Sku is null) 742 | { 743 | return false; 744 | } 745 | } 746 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(left.Sku, right.Sku, context)) 747 | { 748 | return false; 749 | } 750 | 751 | if (!DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualDecimal(left.LineTotal, right.LineTotal, context)) 752 | { 753 | return false; 754 | } 755 | 756 | if (!left.Qty.Equals(right.Qty)) 757 | { 758 | return false; 759 | } 760 | 761 | return true; 762 | } 763 | 764 | private readonly struct __Cmp__global__DeepEqual_Generator_Benchmarking_Order__M_global__DeepEqual_Generator_Benchmarking_Customer_Orders : DeepEqual.Generator.Shared.IElementComparer 765 | { 766 | public __Cmp__global__DeepEqual_Generator_Benchmarking_Order__M_global__DeepEqual_Generator_Benchmarking_Customer_Orders(System.Collections.Generic.IEqualityComparer _ = null) {} 767 | [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining | System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] 768 | public bool Invoke(global::DeepEqual.Generator.Benchmarking.Order l, global::DeepEqual.Generator.Benchmarking.Order r, DeepEqual.Generator.Shared.ComparisonContext c) 769 | { 770 | return DeepEqual.Generator.Shared.ComparisonHelpers.DeepComparePolymorphic(l, r, c); 771 | } 772 | } 773 | 774 | private readonly struct __Cmp__object__M_global__DeepEqual_Generator_Benchmarking_MidGraph_Extra_Val : DeepEqual.Generator.Shared.IElementComparer 775 | { 776 | public __Cmp__object__M_global__DeepEqual_Generator_Benchmarking_MidGraph_Extra_Val(System.Collections.Generic.IEqualityComparer _ = null) {} 777 | [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining | System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] 778 | public bool Invoke(object l, object r, DeepEqual.Generator.Shared.ComparisonContext c) 779 | { 780 | return DeepEqual.Generator.Shared.DynamicDeepComparer.AreEqualDynamic(l, r, c); 781 | } 782 | } 783 | 784 | private readonly struct __Cmp__decimal__M_global__DeepEqual_Generator_Benchmarking_MidGraph_PriceIndex_Val : DeepEqual.Generator.Shared.IElementComparer 785 | { 786 | public __Cmp__decimal__M_global__DeepEqual_Generator_Benchmarking_MidGraph_PriceIndex_Val(System.Collections.Generic.IEqualityComparer _ = null) {} 787 | [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining | System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] 788 | public bool Invoke(decimal l, decimal r, DeepEqual.Generator.Shared.ComparisonContext c) 789 | { 790 | return DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualDecimal(l, r, c); 791 | } 792 | } 793 | 794 | private readonly struct __Cmp__global__DeepEqual_Generator_Benchmarking_Customer__M_global__DeepEqual_Generator_Benchmarking_MidGraph_Customers : DeepEqual.Generator.Shared.IElementComparer 795 | { 796 | public __Cmp__global__DeepEqual_Generator_Benchmarking_Customer__M_global__DeepEqual_Generator_Benchmarking_MidGraph_Customers(System.Collections.Generic.IEqualityComparer _ = null) {} 797 | [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining | System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] 798 | public bool Invoke(global::DeepEqual.Generator.Benchmarking.Customer l, global::DeepEqual.Generator.Benchmarking.Customer r, DeepEqual.Generator.Shared.ComparisonContext c) 799 | { 800 | return DeepEqual.Generator.Shared.ComparisonHelpers.DeepComparePolymorphic(l, r, c); 801 | } 802 | } 803 | 804 | private readonly struct __Cmp__string__M_global__DeepEqual_Generator_Benchmarking_Order_Meta_Val : DeepEqual.Generator.Shared.IElementComparer 805 | { 806 | public __Cmp__string__M_global__DeepEqual_Generator_Benchmarking_Order_Meta_Val(System.Collections.Generic.IEqualityComparer _ = null) {} 807 | [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining | System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] 808 | public bool Invoke(string l, string r, DeepEqual.Generator.Shared.ComparisonContext c) 809 | { 810 | return DeepEqual.Generator.Shared.ComparisonHelpers.AreEqualStrings(l, r, c); 811 | } 812 | } 813 | 814 | private readonly struct __Cmp__global__DeepEqual_Generator_Benchmarking_OrderLine__M_global__DeepEqual_Generator_Benchmarking_Order_Lines : DeepEqual.Generator.Shared.IElementComparer 815 | { 816 | public __Cmp__global__DeepEqual_Generator_Benchmarking_OrderLine__M_global__DeepEqual_Generator_Benchmarking_Order_Lines(System.Collections.Generic.IEqualityComparer _ = null) {} 817 | [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining | System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] 818 | public bool Invoke(global::DeepEqual.Generator.Benchmarking.OrderLine l, global::DeepEqual.Generator.Benchmarking.OrderLine r, DeepEqual.Generator.Shared.ComparisonContext c) 819 | { 820 | return DeepEqual.Generator.Shared.ComparisonHelpers.DeepComparePolymorphic(l, r, c); 821 | } 822 | } 823 | 824 | } 825 | static class __MidGraphDeepEqual_ModuleInit 826 | { 827 | [System.Runtime.CompilerServices.ModuleInitializer] 828 | internal static void Init() 829 | { 830 | _ = typeof(MidGraphDeepEqual); 831 | } 832 | } 833 | } 834 | 835 | ``` 836 | --------------------------------------------------------------------------------