AlexanderIvanov.LazyEqualities 1.2.0

dotnet add package AlexanderIvanov.LazyEqualities --version 1.2.0
NuGet\Install-Package AlexanderIvanov.LazyEqualities -Version 1.2.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AlexanderIvanov.LazyEqualities" Version="1.2.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add AlexanderIvanov.LazyEqualities --version 1.2.0
#r "nuget: AlexanderIvanov.LazyEqualities, 1.2.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install AlexanderIvanov.LazyEqualities as a Cake Addin
#addin nuget:?package=AlexanderIvanov.LazyEqualities&version=1.2.0

// Install AlexanderIvanov.LazyEqualities as a Cake Tool
#tool nuget:?package=AlexanderIvanov.LazyEqualities&version=1.2.0

Lazy Equalities

Lazy Equalities is a simple to use utility library designed to ease .NET developers in implementing object comparison functionality: Equals(object obj), == and !=. Since doing so error-freely for many types is a tedious task of writing lines upon lines of generic boilerplate code, Lazy Equalities aims to do (part of) the job for you.

Some Examples

An example of Lazy Equality comparison with minimal boilerplate:

using AlexanderIvanov.LazyEqualities;

class SomeClass
{
    public int X { get; }
    public int Y { get; }
    public AnotherClass Z { get; }
    public int NewlyAddedProperty { get; }

	public override int GetHashCode() => base.GetHashCode();
    public override bool Equals(object obj) => this == obj as SomeClass;
    public static bool operator ==(SomeClass x, SomeClass y) => LazyEquality.Equals(x, y);
    public static bool operator !=(SomeClass x, SomeClass y) => LazyEquality.NotEquals(x, y);
}
//Neater, isn't it?

Usage in details

Comparison Rules

  • All definitions required for the usage of this library are located in the AlexanderIvanov.LazyEqualities namespace.
  • The Equals and NotEquals methods having a generic interface like this:
    public static bool LazyEquality.Equals<T> (T x, T y) and will be referred to as 'the two comparison methods'.
  • Comparison of x and y will be performed according to the first matched rule:
    • The value of x==y or x!=y will be returned if the generic parameter is a primitive type, string or an enum type.
    • If T implements the System.IEquatable<T> interface:
      • If the defining class of T has an EquatableDependsOnLazyComparisonAttrobute attribute, the IEquatable<T> interface of T will NOT be considered.
      • Otherwise, the system will consider the absence of such attribute:
        • If this is NOT a direct invocation of one of the two comparison methods, the value of (x as IEquatable<T>).Equals(y) or its opposite will be returned.
        • If this is a direct invocation of one of the two comparison methods, an exception will be thrown.
        • This attribute implies that the implementation of IEquatable<T> depends on Lazy Equalities. Its lack thereof means that the implementation is either a native boilerplate or uses another library. The above exception is in place in order to prevent a stack overflow when LazyEquality.Equals<T>(T, T) is called and in turn it calls itself.
    • If T implements a System.Collections.Generic.IEnumerable<TItem> interface or is such interface itself:
      • A sequential equality check will be performed, each item being compared (Recursively, following the same ruleset)
      • Two collections with a different item count will never be considered equal.
      • If T implements IEnumerable<TItem> for more than one different TItem, only the first one will be considered.
    • If T only implements the non-generic System.Collections.IEnumerable interface:
      • A sequential equality check will be performed, each item being compared with the default x.CompareTo(object obj) method (not recursively), whether or not it's been overridden and implemented somehow (using lazy equalities, boilerplate code or otherwise);
      • Two collections with a different item count will never be considered equal.
    • In all other cases, a recursive memberwise comparison will be performed:
      • The values of all non-static properties with a getter that do not have an EqualityExcludeAttribute will be considered.
      • The values of all non-static fields that do have an EqualityIncludeAttribute will be considered

Some Examples (Constructors omitted)

using AlexanderIvanov.LazyEqualities;
using System.Threading;
using System;
using ...;

public class SomeClass
{
	//This field will not be considered because it's static
	private static IDCounter = 0;
	
	//...And neither will this static property
	static string Identifier{ get=>nameof(SomeClass); set=>throw new Exception("WtfuDo?"); }

	//this field will NOT be considered (implicitly, because it's a field)
	internal int objectID= Interlocked.Increment(ref IDCounter);
	
	//but this one will (explicitly, using an attribute)
	[EqualityInclude]
	private int x=12;

	//These three properties will be considered (implicitly)
    protected internal int X { get; internal set; }
    private int Y { get; set; }
    public AnotherClass Z { get; } //The value of this one will be compared recursively
    
    //This property will not be considered (explicitly, using an attribute)
    [EqualityExclude]
    public int NewlyAddedProperty { get; protected set; }
    //This property will not be considered because it doesn't have a getter.
    internal ISomeInterface SetterInjection{ set=> Console.WriteLine("Please don't do this."); } 

  	//It's good practice for non-internal classes to implement these for more seamless 
  	//comparisons.
	public override int GetHashCode() => base.GetHashCode();
    public override bool Equals(object obj) => this == obj as SomeClass;
    public static bool operator ==(SomeClass x, SomeClass y) => LazyEquality.Equals(x, y);
    public static bool operator !=(SomeClass x, SomeClass y) => LazyEquality.NotEquals(x, y);
}
//This class doesn't have an EquatableDependsOnLazyComparisonAttribute
//Because the Equals method doesn't use the lazy comparison library.
internal class NativeEquatable : IEquatable<NativeEquatable>
{
        private string str;
        public bool Equals(NativeEquatable other) => str.Equals(other.str);
}

//But this one does because it...does!
[EquatableDependsOnLazyComparisonAttribute]
internal class NativeEquatableWrapper : IEquatable<NativeEquatableWrapper>
{
	//this one will be compared using ==
    public string StringValue { get; }
    
    //this one will be compared using IEquatable<NativeEquatable>.Equals(NativeEquatable).
    public NativeEquatable Native { get; }
												
    public bool Equals(NativeEquatableWrapper other) => 
    	LazyEquality.Equals(this, other);//<---RIGHT HERE
}

How it works

On every invocation of a comparison method, a corresponding NotEquals<T>.Compare(T x, T y) method will be called. Note that the internal static NotEquals<T> is generic which allows per-type delegate storage using a static field initialized upon first usage.

Said delegates are compiled using reflection and expression trees (once per type) . Reflection is not used upon further comparisons for the same type. This delegate caching technique is often used by dependency injection (DI) frameworks. It is to be expected that performance it always hurts the first time but subsequent invocations are seamless.

The LazyEquality class also exposes the EnsureInitialized(Type type) method which will ensure the comparator delegate for type has been initialized.

This library is written under .Net Standard 2.0 and should work across .Net Core and recent versions of the Windows-only .Net.

GitHub repo

NuGet Package

© Alexander "startrunner" Ivanov 2017 - 2018

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

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
1.2.0 1,169 12/18/2017
1.0.0 1,458 12/17/2017

Early Release