Skip to content
Back to skills

generator.equals

ASecurity

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

  • 192 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 20, 2026
toolsc#git

Security analysis

A100/100

Scanned September 20, 2026

npx -y skills add diegofrata/Generator.Equals --skill skills --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of generator.equals?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for generator.equals
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/diegofrata-generator-equals/badge)](https://www.skillsdirectory.com/skills/diegofrata-generator-equals)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: generator.equals
description: Guidance for using Generator.Equals — a C# source generator for auto-generating Equals, GetHashCode, operators, and Diff/Inequalities methods via attributes.
packages: Generator.Equals
---

# 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:

```xml
<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)

```csharp
[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

```csharp
// 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.

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

### Use Explicit mode when only a few properties matter

```csharp
[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):

```csharp
[DefaultEquality]
private int _version;
```

### Use the generated EqualityComparer

Every `[Equatable]` type gets a nested `EqualityComparer` class:

```csharp
// 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

```csharp
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

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

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

### Record with collections

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

### Struct with custom comparer

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

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

### Inheritance

```csharp
[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

```csharp
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]`).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…