npx agentspec-cli install 4b4e3dab-e91e-4570-b66a-5aa6d9f0f4c9 --framework ClaudeCode→ .claude/skills/generatorequals/SKILL.md
# 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]`).
Built on the Agent Skills standard
General
GitHub Copilot instructions for AI-assisted development.
by AgentHub Crawler
csharp-mcp-server-generator
Generate a complete MCP server project in C# with tools, prompts, and proper configuration
by AgentHub Crawler
code-exemplars-blueprint-generator
Technology-agnostic prompt generator that creates customizable AI prompts for scanning codebases and identifying high-quality code exemplars. Supports multiple programming languages (.NET, Java, JavaScript, TypeScript, React, Angular, Python) with configurable analysis depth, categorization methods, and documentation formats to establish coding standards and maintain consistency across development teams.
by AgentHub Crawler
Sign in to leave a comment.
csharp-tunit
Get best practices for TUnit unit testing, including data-driven tests
by AgentHub Crawler