MintPlayer.ValueComparerGenerator
12.1.0
dotnet add package MintPlayer.ValueComparerGenerator --version 12.1.0
NuGet\Install-Package MintPlayer.ValueComparerGenerator -Version 12.1.0
<PackageReference Include="MintPlayer.ValueComparerGenerator" Version="12.1.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="MintPlayer.ValueComparerGenerator" Version="12.1.0" />
<PackageReference Include="MintPlayer.ValueComparerGenerator"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add MintPlayer.ValueComparerGenerator --version 12.1.0
#r "nuget: MintPlayer.ValueComparerGenerator, 12.1.0"
#:package MintPlayer.ValueComparerGenerator@12.1.0
#addin nuget:?package=MintPlayer.ValueComparerGenerator&version=12.1.0
#tool nuget:?package=MintPlayer.ValueComparerGenerator&version=12.1.0
Value-equality generator
Generates value equality for the models of your incremental source generators, so that every pipeline step caches.
Roslyn compares the output of every incremental step with EqualityComparer<T>.Default. A plain class compares by
reference, so a model that the transform builds anew on every run always looks changed, and everything downstream
re-runs on every keystroke. Put [GenerateEquality] on the model and the generator writes IEquatable<T>,
Equals(object) and GetHashCode() on the type itself: the model is value-equal everywhere, in any step, inside a
Combine tuple, as a property of another model, in a HashSet and in Assert.Equal. No comparer is passed anywhere.
Getting started
Install both MintPlayer.ValueComparerGenerator and MintPlayer.ValueComparerGenerator.Attributes in your project. The generated code calls MintPlayer.SourceGenerators.Tools.ValueEquality, so the project also references MintPlayer.SourceGenerators.Tools, which every generator built on its IncrementalGenerator already does.
Example
The type, and every type containing it, must be partial.
using MintPlayer.ValueComparerGenerator.Attributes;
namespace Demo;
[GenerateEquality]
public sealed partial class CliOption
{
public string Name { get; set; } = "";
public IReadOnlyList<string> Aliases { get; set; } = new List<string>();
public ImmutableArray<Child> Children { get; set; }
[EqualityIgnore]
public string CachedText { get; set; } = "";
}
[GenerateEquality]
public sealed partial class Child
{
public string Value { get; set; } = "";
}
generates (every model's members go into one file, GeneratedEquality.g.cs):
partial class CliOption : global::System.IEquatable<global::Demo.CliOption>
{
public bool Equals(global::Demo.CliOption? other)
{
if (other is null) return false;
if (global::System.Object.ReferenceEquals(this, other)) return true;
// CachedText: [EqualityIgnore]
return global::System.String.Equals(Name, other.Name, global::System.StringComparison.Ordinal)
&& global::MintPlayer.SourceGenerators.Tools.ValueEquality.List<string>(Aliases, other.Aliases)
&& global::MintPlayer.SourceGenerators.Tools.ValueEquality.ImmutableArray<global::Demo.Child>(Children, other.Children);
}
public override bool Equals(object? obj) => Equals(obj as global::Demo.CliOption);
public override int GetHashCode()
{
unchecked
{
int h = 17;
h = h * 31 + (Name is null ? 0 : global::System.StringComparer.Ordinal.GetHashCode(Name));
h = h * 31 + global::MintPlayer.SourceGenerators.Tools.ValueEquality.ListHash<string>(Aliases);
h = h * 31 + global::MintPlayer.SourceGenerators.Tools.ValueEquality.ImmutableArrayHash<global::Demo.Child>(Children);
return h;
}
}
}
GetHashCode() hashes exactly the properties that Equals compares, so equal models always hash equally.
How each property is compared
The comparison is chosen from the declared property type at generation time; nothing is looked up at run time.
| Declared property type | Equality |
|---|---|
string |
ordinal |
| primitive, enum | == |
T[] |
element-wise, in order |
ImmutableArray<T> |
element-wise; default equals only default, never an empty array |
List<T>, IList<T>, IReadOnlyList<T> |
element-wise, in order, whatever the runtime type |
IEnumerable<T>, IReadOnlyCollection<T>, other sequences |
element-wise, in order |
IDictionary<K,V>, IReadOnlyDictionary<K,V> |
same keys with equal values, in any order |
ValueTuple<...> |
item by item, applying this table to each item |
| another model, or any other type | EqualityComparer<T>.Default |
Nested shapes compose: an ImmutableArray<ImmutableArray<Model>>, a List<(string, List<Model>)> or a tuple holding a
list all compare structurally.
Supported shapes
Sealed and non-sealed classes, abstract hierarchies, records (sealed or derived), structs, record structs, generic types, and types nested in any of these.
- Hierarchies. Every type deriving from a
[GenerateEquality]type gets its own members too, whether or not it carries the attribute, so it must bepartialas well. An instance is never equal to an instance of another runtime type. - Equality you wrote yourself. A member the type already declares (
Equals(T),Equals(object),GetHashCode()) is not generated (MINT002).
Attributes
[GenerateEquality]on a class, record or struct: generate the members.[EqualityIgnore]on a property: leave it out ofEqualsandGetHashCode.[UseEqualityComparer(typeof(C))]on a property: compare it withC, anIEqualityComparer<TProperty>with a staticInstanceor a public parameterless constructor. For example[UseEqualityComparer(typeof(JObjectValueComparer))]from MintPlayer.ValueComparers.NewtonsoftJson.
Diagnostics
| Id | Severity | Meaning |
|---|---|---|
MINT001 |
Error | A model property holds a Roslyn type (ISymbol, SyntaxNode, Location, Compilation, SemanticModel, ...), directly or through a collection, tuple or nested class. It pins the compilation between runs. Project it to a string, a LocationKey or a PathSpec. |
MINT002 |
Info (Warning when only one of Equals(object)/GetHashCode() is declared) |
The type declares an equality member itself, so it was not generated. |
MINT003 |
Error | A model, or a type deriving from one, is not partial. |
MINT004 |
Error | The [UseEqualityComparer] type is not an IEqualityComparer<T> of the property type, or cannot be created. |
MINT005 |
Warning | A property type has only reference equality (a class that neither overrides Equals nor implements IEquatable<T>, and is not a model or a supported collection). Make it equatable, mark it [GenerateEquality], or use [UseEqualityComparer]. |
MINT006 |
Error | A type containing a model is not partial. |
Using the models in a pipeline
No comparer is needed on any step. A model coming out of a transform, a Collect() of models, or a Combine tuple
holding models all compare by value under the default comparer.
The one exception is a step that builds a new collection from its input, such as a Select after Collect()
that filters or projects, or a transform returning an array. An array or ImmutableArray<T> compares by reference, so
that step would report Modified on every run. Return an EquatableArray<T> (from MintPlayer.SourceGenerators.Tools)
instead:
var registrationsProvider = classesProvider
.Combine(assemblyLevelProvider)
.Select(static (p, ct) => p.Left.Concat(p.Right).ToEquatableArray());
Joining providers
The MintPlayer.SourceGenerators.Tools package already contains 4 extension methods that help you join providers.
var sourceProvider = typeProvider
.Join(typeTreeProvider)
.Join(childrenWithoutDerived)
.Join(settingsProvider)
.Join(hasCodeAnalysisReference)
.Select(static Producer (p, ct) => new TreeProducer(p.Item1, p.Item2, p.Item3, p.Item4.RootNamespace!, p.Item5));
By using this extension method, you can avoid the nested tuples that would require you to write p.Left.Left.Left.Left.Right to access a value.
If you want to join more than 5 providers, apply [assembly: GenerateJoinMethods(n)]. The ValueComparerGenerator
package then generates the overloads for 6 to n providers into JoinMethods.g.cs. They chain the same way:
previous.Join(next). The generated class is internal, so every generator project gets its own copy and none of
them is exposed to projects that reference it. Without the attribute, nothing is generated.
Running your generator in-process from a test project
A test project can reference a generator that uses [GenerateEquality] with a plain <ProjectReference>, to run it
in-process. The attributes dll is added to that project's bin folder, but not to its deps.json. The generated
equality code doesn't need it. Code that reflects over the models' attributes, such as GetCustomAttributes(), does.
The classic VSTest testhost resolves the dll from bin anyway. Exe-hosted runners (xunit.v3, Microsoft.Testing.Platform)
and ordinary apps throw FileNotFoundException. If your project reflects over the models' attributes, add:
<PackageReference Include="MintPlayer.ValueComparerGenerator.Attributes" Version="12.1.0" />
Breaking changes in 12.0.0
The full list, with a migration guide, is in the changelog.
- The attributes are renamed, because they no longer generate comparers:
[AutoValueComparer]is now[GenerateEquality](GenerateEqualityAttribute) and[ComparerIgnore]is now[EqualityIgnore](EqualityIgnoreAttribute). There are no aliases.[UseEqualityComparer]and[GenerateJoinMethods]keep their names, and the package ids stayMintPlayer.ValueComparerGeneratorandMintPlayer.ValueComparerGenerator.Attributes. - The generator no longer emits an
XValueComparerclass, a[ValueComparer]tag, or the.WithComparer()/.WithNullableComparer()extension methods. The members are generated on the model itself. Delete every.WithComparer()/.WithNullableComparer()call; where a step builds a new collection, returnEquatableArray<T>. - Records, structs, record structs and generic models are now supported.
- A derived type is now covered even without its own attribute, and must be
partial(MINT003). ComparerRegistry.Registeris replaced by[UseEqualityComparer]on the property.MINT001inspects model properties instead of.WithComparer()calls.- Dictionaries compare order-insensitively.
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- MintPlayer.ValueComparerGenerator.Attributes (>= 12.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 12.1.0 | 108 | 9/26/2026 | |
| 12.0.1 | 59 | 9/25/2026 | |
| 12.0.0 | 61 | 9/25/2026 | |
| 11.0.0 | 53 | 9/25/2026 | |
| 10.20.2 | 328 | 9/3/2026 | |
| 10.20.1 | 106 | 8/27/2026 | |
| 10.20.0 | 614 | 6/5/2026 | |
| 10.19.0 | 705 | 4/3/2026 | |
| 10.13.0 | 136 | 1/19/2026 | |
| 10.10.1 | 126 | 1/12/2026 | |
| 10.10.0 | 142 | 1/12/2026 | |
| 10.9.0 | 133 | 1/12/2026 | |
| 10.8.0 | 216 | 12/24/2025 | |
| 10.7.0 | 310 | 11/13/2025 | |
| 10.5.0 | 333 | 11/11/2025 | |
| 10.4.0 | 301 | 11/10/2025 | |
| 10.3.0 | 226 | 11/6/2025 | |
| 10.2.0 | 231 | 11/6/2025 | |
| 10.1.0 | 220 | 11/5/2025 | |
| 10.0.1 | 232 | 11/2/2025 |