Generator.Equals

SkillDev tools

Guidance for using Generator.Equals — a C# source generator for auto-generating Equals, GetHashCode, operators, and Diff/Inequalities methods via attributes.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Generator.Equals skill

What this skill tells your AI

The instructions your AI receives, as published by diegofrata/generator.equals in skills/SKILL.md and read by ahel’s review.

C# source generator that auto-implements IEquatable<T>, Equals(), GetHashCode(), ==/!= operators, and an Inequalities() diff method at compile time using attributes. No runtime reflection.

Setup

Add both packages:

<PackageReference Include="Generator.Equals" PrivateAssets="all" />
<PackageReference Include="Generator.Equals.Runtime" />

Requires C# 9.0+. The type must be partial.

Attributes Quick Reference

Type-level

AttributePurpose
[Equatable]Generates equality members for the type
[Equatable(Explicit = true)]Only compare properties with explicit equality attributes
[Equatable(IgnoreInheritedMembers = true)]Skip base class members entirely
[Equatable(GenerateClassEqualityOperators = false)]Do not generate ==/!= for a class; leave == as reference equality

Property/Field-level

AttributeUse When
[DefaultEquality]Default comparer. Required on fields (fields are opt-in; properties are compared by default).
[IgnoreEquality]Skip this member
[OrderedEquality]Compare collection elements in order (like SequenceEqual)
[UnorderedEquality]Compare collection elements ignoring order
[SetEquality]Compare as sets (duplicates ignored)
[ReferenceEquality]Compare by reference only
[StringEquality(StringComparison.X)]String-specific comparison (string props only)
[PrecisionEquality(0.001)]Tolerance-based numeric comparison
[CustomEquality(typeof(MyComparer))]Custom IEqualityComparer<T>

Comparer constructor patterns (for collection and custom attributes)

[OrderedEquality]                                                          // Default comparer
[OrderedEquality(typeof(MyComparer))]                                     // Type with static Default member
[OrderedEquality(typeof(StringComparer), nameof(StringComparer.Ordinal))] // Named static member
[OrderedEquality(StringComparison.OrdinalIgnoreCase)]                     // StringComparison shorthand

Best Practices

Always annotate collection properties

// WRONG - produces diagnostic GE001
public List<int> Items { get; set; }

// RIGHT
[OrderedEquality]
public List<int> Items { get; set; }

Two exceptions: a collection type that is itself [Equatable] already compares structurally, so no attribute is needed. And [DefaultEquality] opts the member into the type's own Equals — use it for collections that implement their own value equality.

[DefaultEquality]
public MyImmutableList<int> Items { get; set; }   // MyImmutableList compares structurally itself

Use Explicit mode when only a few properties matter

[Equatable(Explicit = true)]
partial class User
{
    [DefaultEquality] public string Id { get; set; }     // Compared
    public string DisplayName { get; set; }               // Ignored
    public DateTime LastLogin { get; set; }               // Ignored
}

Mark fields explicitly

Fields are opt-in (since v5): they are not compared unless annotated. Properties are compared by default. To include a field, annotate it with [DefaultEquality] (or another equality attribute):

[DefaultEquality]
private int _version;

Use the generated EqualityComparer

Every [Equatable] type gets a nested EqualityComparer class:

// Use in dictionaries, HashSets, LINQ
var dict = new Dictionary<MyType, string>(MyType.EqualityComparer.Default);
var distinct = items.Distinct(MyType.EqualityComparer.Default);

Use Inequalities for diff/audit

foreach (var diff in MyType.EqualityComparer.Default.Inequalities(oldObj, newObj))
    Console.WriteLine(diff);
// Output: Name: John -> Jane
// Output: Addresses["home"].Street: 123 Main St -> 456 Oak Ave

Nested [Equatable] objects in collections are auto-drilled — you get per-field diffs, not "entire object changed".

Common Pitfalls

  1. Non-partial type (GE006) — The type MUST be declared partial.

  2. Manual Equals/GetHashCode (GE005) — Do NOT override Equals() or GetHashCode() manually on an [Equatable] type. The generator owns these.

  3. Conflicting collection attributes (GE007) — Only ONE of [OrderedEquality], [UnorderedEquality], [SetEquality] per property.

  4. Hash code instability[UnorderedEquality], [SetEquality], and [PrecisionEquality] return hash code 0 or exclude from hashing. Types using these are poor dictionary keys.

  5. Inheritance with [Equatable] — The generator walks the full inheritance chain. If any ancestor has [Equatable] or overrides Equals(), it calls base.Equals(). Use IgnoreInheritedMembers = true to opt out.

  6. Overriding properties inherits attributes — A Child overriding a Parent's [OrderedEquality] virtual int[] Values automatically inherits [OrderedEquality]. Do NOT re-annotate unless you want to change behavior.

  7. [StringEquality] on non-string (GE008) — Only valid on string properties.

  8. [PrecisionEquality] types (GE010) — Only float, double, decimal, int, long, short, sbyte and their nullable variants.

  9. GenerateClassEqualityOperators is class-only (GE011) — Records always get compiler-generated operators; structs always get generated ones, since C# predefines == for reference types but not for user-defined structs — dropping them would make every == on the type a CS0019 error, with no reference-equality fallback like a class has.

  10. Opting out of operators must cover the whole chain (GE012) — C# operator overload resolution considers base-type operators, so GenerateClassEqualityOperators = false on a derived class alone does nothing. Set it on the root of the [Equatable] chain too.

Examples

Basic class

[Equatable]
partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    [IgnoreEquality]
    public DateTime LastUpdated { get; set; }
}

Record with collections

[Equatable]
partial record Customer(
    string Id,
    string Name,
    [property: OrderedEquality] ImmutableArray<Address> Addresses,
    [property: UnorderedEquality] ImmutableDictionary<string, string> Tags
);

Struct with custom comparer

[Equatable]
partial struct Coordinate
{
    [PrecisionEquality(0.0001)]
    public double Latitude { get; set; }

    [PrecisionEquality(0.0001)]
    public double Longitude { get; set; }
}

Inheritance

[Equatable]
partial class Animal
{
    public string Species { get; set; }
}

[Equatable]
partial class Pet : Animal
{
    public string Name { get; set; }
    // Species is also compared via base.Equals()
}

Diff / Inequalities

var diffs = Customer.EqualityComparer.Default.Inequalities(before, after);
foreach (var d in diffs)
{
    // d.Path  — e.g., "Addresses[0].Street"
    // d.Left  — old value
    // d.Right — new value
    Console.WriteLine(d);
}

Diagnostics

CodeDescription
GE001Collection missing equality attribute
GE002Complex property missing [Equatable]
GE003Collection element missing [Equatable]
GE004Equality attribute used but containing type lacks [Equatable]
GE005Manual Equals/GetHashCode with [Equatable]
GE006[Equatable] on non-partial type
GE007Conflicting equality attributes
GE008[StringEquality] on non-string
GE009Collection attribute on non-collection
GE010[PrecisionEquality] on unsupported type
GE011GenerateClassEqualityOperators on a record or struct
GE012GenerateClassEqualityOperators = false defeated by a base type's operators

GE001, GE002, GE003, and GE006 have automatic code fixes. Every diagnostic can be suppressed the usual way (#pragma warning disable, .editorconfig, or [SuppressMessage]).

Signals

GitHub stars
192
Forks
24
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
generator-equals
Source
github.com/diegofrata/generator.equals