diegofrata / generator.equals Goto Github PK
View Code? Open in Web Editor NEWA source code generator for automatically implementing IEquatable<T> using only attributes.
License: MIT License
A source code generator for automatically implementing IEquatable<T> using only attributes.
License: MIT License
This would require a breaking change, so I am adding this to the 3.0 milestone.
The idea would be to make the method IEquatable<T>.Equals(T)
virtual and have a similar implementation to records, where the immediate base is overridden and sealed, calling Equals(object?). Below is the decompiled source of a derived record -- that's what we want for classes.
[NullableContext(2)]
[CompilerGenerated]
public override bool Equals(object obj)
{
return this.Equals(obj as Derived);
}
[NullableContext(2)]
[CompilerGenerated]
public override sealed bool Equals(Base other)
{
return this.Equals((object) other);
}
[NullableContext(2)]
[CompilerGenerated]
public virtual bool Equals(Derived other)
{
if ((object) this == (object) other)
return true;
return base.Equals((Base) other) && EqualityComparer<string>.Default.Equals(this.\u003CName\u003Ek__BackingField, other.\u003CName\u003Ek__BackingField);
}
First of: Very useful generator. Thanks for building this! :)
I ran into a small issue with classes that have indexers:
[Equatable]
partial class Sample
{
public string Property { get; set; }
public string this[int index] => index.ToString();
}
The generated code looks like this: (shortened for readability)
partial class Sample : IEquatable<Sample> {
// ...
public bool Equals(Sample? other) {
return !ReferenceEquals(other, null) && this.GetType() == other.GetType()
&& EqualityComparer<String>.Default.Equals(Property!, other.Property!)
&& EqualityComparer<String>.Default.Equals(this!, other.this!) // <- invalid
;
}
// ...
public override int GetHashCode() {
var hashCode = new global::System.HashCode();
hashCode.Add(this.GetType());
hashCode.Add(this.Property!, EqualityComparer<String>.Default);
hashCode.Add(this.this!, EqualityComparer<String>.Default); // <- invalid
return hashCode.ToHashCode();
}
which is then rejected by the compiler with CS1001 and CS1003.
It seems that Generator.Equals
thinks that this
is the name of a property here, rather than interpreting it as the keyword indicating an indexer.
As a workaround, I can mark the indexer with [IgnoreEquality]
. However, I think it would be better if Generator.Equals
simply ignored indexers by default.
We can juse use UnorderedSequenceEquality
, since IDictionary<TKey, TValue>
extends IEnumerable<KeyValuePair<TKey, TValue>>
, and KeyValuePair<TKey, TValue>
implements Equals
and HashCode
correctly.
using Generator.Equals;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Test
{
[Equatable]
partial record MyRecord(
[property: UnorderedSequenceEquality] Dictionary<string, int> Mutable,
[property: UnorderedSequenceEquality] ImmutableDictionary<string, int> Immutable
);
class Program
{
static void Main()
{
var mutable1 = new Dictionary<string, int> { { "A", 0 }, { "B", 42 } };
var mutable2 = new Dictionary<string, int> { { "A", 0 }, { "B", 42 } };
var record1 = new MyRecord(mutable1, mutable1.ToImmutableDictionary());
var record2 = new MyRecord(mutable2, mutable2.ToImmutableDictionary());
Console.WriteLine(record1 == record2);
}
}
}
Hi. Cool generator.
I am migrating to your generator from this - https://github.com/tom-englert/Equatable.Fody. It uses Fody and explicitly defined properties to implement IEquatable. I would like to see the same mode in this generator, in which you need to explicitly specify the necessary properties.
It would also be great to think about supporting IIncrementalGenerator
I've recently found this package and wanted to replace own reflection-based equality. I marked up classes but got no result. Building project I've got warning:
CSC : warning CS8785: Generator 'EqualsGenerator' failed to generate source. It will not contribute to the output and compilation errors may occur as a result. Exception was of type 'InvalidOperationException' with message 'Nullable object must have a value'
So, error message wasn't explanatory enough. After quite a bit of debugging, I've found out that my class contained IEnumerable<T>
property with UnorderedEquality
attribute. It caused NullReferenceException during generation.
I believe, it is a bug, isn't it? If so, I can provide some fix.
P.S. Thanks for your work
While experimenting with Generator.Equals, I found that if I try to customize string equality to be case insensitive for a property that is for example an array of strings, the code generated by Generator.Equals will currently incorrectly assume the property is a string. GetHashCode code produces a compilation error:
CS0411: The type arguments for method 'HashCode.Add(T, IEqualityComparer?)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
I assume the only way Generator.Equals can handle this case is via a custom equality, but I believe the case to be common enough to consider supporting it directly. Making StringEqualityAttribute handle it seems like it would be nice. Other than that, in general it would be nice if generation failed with a better error when an attribute is used with a member of an unsupported type.
Moreover, if I try to combine StringEqualityAttribute and UnorderedEqualityAttribute on the same property, generation will obey unordered equality and the intent to treat the string elements as case insensitive will be ignored. Personally I think this is also a compelling scenario to support.
using Generator.Equals;
Console.WriteLine("Hello, World!");
[Equatable]
public partial class Resource
{
[StringEquality(StringComparison.OrdinalIgnoreCase)]
public string[] Tags { get; set; } = Array.Empty<string>();
}
Generator.Equals will detect the attribute and generate code that assumes that the property is a string, for both the code generated for equality and the code generated for hash code:
/// <inheritdoc/>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Generator.Equals", "1.0.0.0")]
protected bool Equals(global::Resource? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.GetType() == this.GetType()
&& global::System.StringComparer.OrdinalIgnoreCase.Equals(this.Tags!, other.Tags!)
;
}
/// <inheritdoc/>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Generator.Equals", "1.0.0.0")]
public override int GetHashCode()
{
var hashCode = new global::System.HashCode();
hashCode.Add(this.GetType());
hashCode.Add(
this.Tags!,
global::System.StringComparer.OrdinalIgnoreCase
);
return hashCode.ToHashCode();
}
I'm not sure, just for fyi
https://github.com/diegofrata/Generator.Equals/blob/main/Generator.Equals/EqualsGenerator.cs#L35
Using Compilation
inside the generator pipeline renders it is non-incremental. Additional details is here zompinc/sync-method-generator#20
Would be great to have a wrapper class such that one can have a Comparer collect the difference between say two record
's or other supported type. This would also walk the nested collection and say use a .
, [x]
or =>
notation for the hierarchy, array/set elements or maps.
A Use Case: CDC you have the before and after image from your database of choice and need detect what changes happen between them, so some conditional biz logic can be applied based upon that.
That this generator avoids reflection means it will also work with AoT compilation.
Classes with static properties, like:
[Equatable]
partial class Sample
{
public string Property { get; set; }
public static string StaticProperty { get; set; }
}
lead to a compiler error when used with Generator.Equals
:
Member 'Sample.StaticProperty' cannot be accessed with an instance reference; qualify it with a type name instead
As a workaround, I can mark static properties with [IgnoreEquality]
. However, I think it would be better if Generator.Equals simply ignored them by default.
And do something at gen-time? Not sure what you can do.
README already has examples as partial obviously but they're easily missed.
Hey,
If you're using XML doc comments and have warnings enabled when they are missing the source generator gets picked up (even though you've correctly added GeneratedCodeAttribute
which you'd expect to be skipped but isn't).
Would you be interested in either:
#pragma warning disable 1591
to the file to avoid the warnings entirely?I'd be happy to contribute a PR for either if it's welcomed ๐
This is actually a follow up to #38 but in another situation.
The ObjectEqualityComparer<T>
class is used, which actually calls x.Equals(y)
where y
is of the expected time, resulting in only the base class Equals implementation being called.
I'm wondering if marking the typed Equals
method as public
might make things harder? If it was protected
instead, it would be much harder to fall in that trap. The method checks for
public class GeneratorBaseClassBugTests
{
[Equatable]
public partial class MyContainer
{
[OrderedEquality]
public MyBase[]? Content { get; set; }
}
[Equatable]
public abstract partial class MyBase
{
}
[Equatable]
public partial class MyImpl : MyBase
{
public int Value { get; set; }
}
[Test]
public void TestEqual()
{
var v1 = new MyContainer { Content = new MyBase[] { new MyImpl { Value = 1 } } };
var v2 = new MyContainer { Content = new MyBase[] { new MyImpl { Value = 1 } } };
Assert.That(v1.Equals(v2), Is.True);
}
[Test]
public void TestNotEqual()
{
var v1 = new MyContainer { Content = new MyBase[] { new MyImpl { Value = 1 } } };
var v2 = new MyContainer { Content = new MyBase[] { new MyImpl { Value = 2 } } };
Assert.That(v1.Equals(v2), Is.False);
}
}
The code ends up calling SequenceEqual
, which again calls GenericEqualityComparer<BaseType>
, ignoring inherited Equals implementations. This is actually the behavior of SequenceEqual
, which makes me think that even outside of your comparison code, the generated equality checks may fail in other circumstances. Let me know if you believe going the protected
route for the typed Equals implementation sounds like the way to go.
.NET 6.0, Generator.Equals 2.7.2
Sorry to be a pain, hopefully that'll avoid some nasty surprises for other people :D Thanks again for your consideration and the great library!
Hi,
the nuget is missing license information so it is not shown in Visual sutdio nor in org.nuget...
you can simply add it by the following property in the csproj/:
<PropertyGroup>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
it this also recommended by microsoft:
https://learn.microsoft.com/en-us/nuget/create-packages/package-authoring-best-practices#licensing
[Equatable]
public partial record struct MyStruct(int Data);
Generates
partial record MyStruct
{
/// <inheritdoc/>
[GeneratedCode("Generator.Equals", "1.0.0.0")]
public bool Equals(MyStruct? other)
{
return
base.Equals(other)
&& EqualityComparer<Int32>.Default.Equals(Data!, other.Data!)
;
}
/// <inheritdoc/>
[GeneratedCode("Generator.Equals", "1.0.0.0")]
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(base.GetHashCode());
hashCode.Add(
Data!,
EqualityComparer<Int32>.Default);
return hashCode.ToHashCode();
}
}
This is correct except it should say partial record struct MyStruct
and the Equals method should be
/// <inheritdoc />
[GeneratedCode("Generator.Equals", "1.0.0.0")]
public bool Equals(MyStruct? other)
{
return
other.HasValue
&& EqualityComparer<Int32>.Default.Equals(Data, other.Value.Data)
;
}
also the GetHashCode
method does not need to do hashCode.Add(base.GetHashCode());
I am willing to have a go at this myself.
Hey there,
I was just wondering if you intended this library to not be licenced since that implies that all rights are reserved and it can't redistribute your code without explict permissions.
Hello,
Would it be possible to also generate equality members for struct
?
There might be some niche cases where you want this:
public class ObjectReferenceEqualityComparer<T> : IEqualityComparer<T>
where T : class
{
private static IEqualityComparer<T> _defaultComparer;
public static IEqualityComparer<T> Default => _defaultComparer ?? (_defaultComparer = new ObjectReferenceEqualityComparer<T>());
bool IEqualityComparer<T>.Equals(T x, T y)
{
return ReferenceEquals(x, y);
}
int IEqualityComparer<T>.GetHashCode(T obj)
{
return RuntimeHelpers.GetHashCode(obj);
}
}
Another idea is to remove the runtime dependency, especially for those who don't use custom Comparers. They can be delivered explicitly (with global::
full typepaths to avoid possible collisions).
Originally posted by @HavenDV in #22 (comment)
Hi! Thank you for this awesome library.
Suppose I have a ViewModel class like this:
using System.IO;
using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using Dapper.Contrib.Extensions;
using Generator.Equals;
namespace CoilQueryTool.ViewModels
{
[Equatable(IgnoreInheritedMembers = true)]
[Table("CoilMap")]
internal partial class CoilRecordViewModel : ObservableObject
{
public CoilRecordViewModel ShallowCopy()
{
return (CoilRecordViewModel)MemberwiseClone();
}
[IgnoreEquality]
public int Id { get; set; }
[ObservableProperty]
private string coilId = null!;
[ObservableProperty]
private string? name;
[ObservableProperty]
private string? connector;
[ObservableProperty]
private string? pN;
[ObservableProperty]
private string? description;
[ObservableProperty]
private string? machine;
[ObservableProperty]
private string? mode;
}
}
Since most of the properties of this class is generated by another code generator - CommunityToolkit.Mvvm's code generator, Generator.Equals will not generate code for these properties
.
I am wondering if [DefaultEquality]
can be used like this:
class MyViewModel
{
[DefaultEquality]
[ObservableProperty]
private string title;
}
Even for a simple usage like this
[Equatable]
internal partial class Class1
{
public string Text { get; set; } = string.Empty;
}
a runtime reference to Generator.Equals.Runtime.dll
is required, just because of the usage of the attribute.
Proposed solution:
Decorate the attributes with e.g. [Conditional("GENERATOR_EQUALS")]
(like e.g. JetBrains does for their annotation attributes: https://www.jetbrains.com/help/resharper/Code_Analysis__Annotations_in_Source_Code.html)
This way a runtime reference should only be needed when using one of the special comparators.
Iโve been using your lib for a while in 0install-dotnet. Great project, thanks!
Starting with v2.7.2 up to and including v2.7.5 my builds targeting .NET Framework 4.7.2 (with <Nullable>annotations</Nullable>
) are throwing exceptions for null values:
System.NullReferenceException with message "Object reference not set to an instance of an object."
at Generator.Equals.DefaultEqualityComparer`1.ObjectEqualityComparer.GetHashCode(T obj)
at System.HashCode.Add[T](T value, IEqualityComparer`1 comparer)
at ZeroInstall.Model.Command.GetHashCode() in /_/src/Model/Generator.Equals/Generator.Equals.EqualsGenerator/ZeroInstall.Model.Command.Generator.Equals.g.cs:line 58
The same code targeting .NET 6.0 (with <Nullable>enable</Nullable>
) still works fine.
Was this an unintentional change or do I need to change something in my usage of the lib?
Issue
When I mark a property as obsolete but don't use [IgnoreEquality]
, then I get build warnings about usages of the obsolete property.
I would expect those to be suppressed because they will go away when the property is removed anyway. In the meantime, I can't use [Obsolete]
on properties in projects where I treat warnings as errors.
Solution 1
Use a pragma to suppress build warnings when comparing obsolete things in the generated source.
Solution 2
Add a toggle on [Equatable]
to decide whether to use a pragma to suppress obsolete warnings. Something like:
[Equatable(SuppressObsoleteWarnings = true)]
public partial record Something
{
public string Thing1 { get; set; }
[Obsolete]
public string Thing2 { get; set; }
}
If you have a preferred way of doing it, I'd be more than happy to write up a PR.
This code will generate equality comparisons:
[DefaultEquality] public bool MyProperty{ get; set; }
or
[DefaultEquality] private bool myProperty;
But the following will not generate any equality code:
[ObservableProperty]
[property: DefaultEquality]
private bool myProperty;
and this will generate the code but it also causes warning message MVVMTK0034:
[ObservableProperty]
[DefaultEquality]
private bool isFromExistingStock;
Reasoning: some libraries (like the MVVM community toolkit) require all attributes to be applied to the backing field, but doing so will introduce warnings whenever you try to access the backing field instead of the property.
I reviewed the source generated by MVVM toolkit, and it appears that (when using [property:DefaultEquality] the auto-generated property will include the DefaultEqualityAttribute, but source code normally generated by Generator.Equals is not included. Maybe attributes generated a source generator cannot be used to generate more code? It would make sense if that was the issue.
/// <inheritdoc cref="myProperty"/>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::Generator.Equals.DefaultEqualityAttribute()]
public bool MyProperty
{
get => myProperty;
set
{
if (!global::System.Collections.Generic.EqualityComparer<bool>.Default.Equals(myProperty, value))
{
OnMyPropertyChanging(value);
OnMyPropertyChanging(default, value);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.MyProperty);
myProperty = value;
OnMyPropertyChanged(value);
OnMyPropertyChanged(default, value);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.MyProperty);
}
}
}
Thoughts?
Create comparer for strings:
[StringEquality(StringComparison.OrdinalIgnoreCase)]
public string Name { get; set; }
using Generator.Equals;
[Equatable]
partial class MyClass
{
[SequenceEquality]
public string[] Fruits { get; set; }
);
The last line should be };
.
Xunit works really well with nullable by using ctor and IDisposable for its setup/teardown semantics. Need to refactor tests to use that and also adopt snapshot tests throughout.
First of all, great library, thanks for making it!
When calling .Equals()
or System.Collections.Generic.EqualityComparer
on a base class that uses [Equatable]
, the generated Equals
implementation will not call inherited Equals
implementations.
[Equatable]
, reference equality is used (which is not what we want)[Equatable]
, the base class only checks it's own fields and ignores inherited types fields.public class GeneratorBaseClassBugTests
{
[Equatable]
public partial class MyContainer
{
public MyBase Content { get; set; }
}
[Equatable]
public abstract partial class MyBase
{
}
[Equatable]
public partial class MyImpl : MyBase
{
public int Value { get; set; }
}
[Test]
public void Test()
{
var v1 = new MyContainer { Content = new MyImpl { Value = 1 } };
var v2 = new MyContainer { Content = new MyImpl { Value = 2 } };
Assert.That(v1, Is.Not.EqualTo(v2));
}
}
This code is generated in the container:
global::System.Collections.Generic.EqualityComparer<global::MyNamespace.MyBase>.Default.Equals(Content!, other.Content!)
This shorts-circuits the concrete type by directly checking the base type. This would work:
global::System.Object.Equals(Content!, other.Content!)
.NET 6.0, Generator.Equals 2.6.0
Thanks!
Currently the generator will break with any types defined in C# top-level statement.
When I run the example from README, I get printed 'False'.
Probably it has something to do with the weird warning I'm getting:
An instance of analyzer Generator.Equals.EqualsGenerator cannot be created from C:\Users\Patrik\.nuget\packages\generator.equals\0.3.0\analyzers\dotnet\cs\Generator.Equals.dll: Could not load file or assembly 'netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' or one of its dependencies. The system cannot find the file specified..
Here's the code: https://github.com/PatrikBak/Bug.Generator.Equals
Visual Studio 2019 Version 16.8.1
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.