Skip to content
cccskills
FreePublished on GitHub

generator.equals

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

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md9.0 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Generator.Equals

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 [OrderedEquality] collections and dictionary values are auto-drilled — you get per-field diffs. [UnorderedEquality]/[SetEquality] members report added/removed elements instead (a null collection versus an empty one is a single difference on the member). A collection member without one of those attributes is reported as one whole-member change.

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. Hashing — [UnorderedEquality] XORs element hashes: order-independent, but elements repeated an even number of times cancel out ([1, 1] hashes like []) and null elements are skipped. [SetEquality] returns 0, and [PrecisionEquality] is excluded from hashing. Types keyed mainly on such members have poor hash distribution.

  5. Inheritance with [Equatable] — The generator walks the full inheritance chain and delegates (base.Equals()/base.GetHashCode()) to the nearest ancestor that owns equality: one with [Equatable]/a generated comparer, any record base, or a class hand-rolling a complete contract — a GetHashCode() override plus either a public Equals(TSelf) (IEquatable<TSelf>) or an Equals(object) override, both declared on that same class. An ancestor with only part of that contract is treated as plain: the walk continues past it and its public properties are compared explicitly, as are those of any plain classes between the type and the owning ancestor. 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]).

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

Report a problem with this skill