Generator.Equals
SkillDev toolsGuidance 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.
No other account needed.
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
| Attribute | Purpose |
|---|---|
[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
| Attribute | Use 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
-
Non-partial type (GE006) — The type MUST be declared
partial. -
Manual Equals/GetHashCode (GE005) — Do NOT override
Equals()orGetHashCode()manually on an[Equatable]type. The generator owns these. -
Conflicting collection attributes (GE007) — Only ONE of
[OrderedEquality],[UnorderedEquality],[SetEquality]per property. -
Hash code instability —
[UnorderedEquality],[SetEquality], and[PrecisionEquality]return hash code 0 or exclude from hashing. Types using these are poor dictionary keys. -
Inheritance with [Equatable] — The generator walks the full inheritance chain. If any ancestor has
[Equatable]or overridesEquals(), it callsbase.Equals(). UseIgnoreInheritedMembers = trueto opt out. -
Overriding properties inherits attributes — A
Childoverriding aParent's[OrderedEquality] virtual int[] Valuesautomatically inherits[OrderedEquality]. Do NOT re-annotate unless you want to change behavior. -
[StringEquality]on non-string (GE008) — Only valid onstringproperties. -
[PrecisionEquality]types (GE010) — Onlyfloat,double,decimal,int,long,short,sbyteand their nullable variants. -
GenerateClassEqualityOperatorsis 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 aCS0019error, with no reference-equality fallback like a class has. -
Opting out of operators must cover the whole chain (GE012) — C# operator overload resolution considers base-type operators, so
GenerateClassEqualityOperators = falseon 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
| Code | Description |
|---|---|
| GE001 | Collection missing equality attribute |
| GE002 | Complex property missing [Equatable] |
| GE003 | Collection element missing [Equatable] |
| GE004 | Equality attribute used but containing type lacks [Equatable] |
| GE005 | Manual Equals/GetHashCode with [Equatable] |
| GE006 | [Equatable] on non-partial type |
| GE007 | Conflicting equality attributes |
| GE008 | [StringEquality] on non-string |
| GE009 | Collection attribute on non-collection |
| GE010 | [PrecisionEquality] on unsupported type |
| GE011 | GenerateClassEqualityOperators on a record or struct |
| GE012 | GenerateClassEqualityOperators = 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