U2U.ValueObjectComparers
0.1.0
See the version list below for details.
dotnet add package U2U.ValueObjectComparers --version 0.1.0
NuGet\Install-Package U2U.ValueObjectComparers -Version 0.1.0
<PackageReference Include="U2U.ValueObjectComparers" Version="0.1.0" />
paket add U2U.ValueObjectComparers --version 0.1.0
#r "nuget: U2U.ValueObjectComparers, 0.1.0"
// Install U2U.ValueObjectComparers as a Cake Addin #addin nuget:?package=U2U.ValueObjectComparers&version=0.1.0 // Install U2U.ValueObjectComparers as a Cake Tool #tool nuget:?package=U2U.ValueObjectComparers&version=0.1.0
Implementing ValueObject's Equality - Efficiently
This blog discusses how to implement a Value Object's Equals method efficiently.
What are Value Objects?
In Domain Driven Design objects are divided into two groups: Entities and Value Objects.
Entities are objects that have an identity and life cycle.
Value Objects are objects that don't have any real identity and are mainly used to describe aspects of an entity, such as your name which is of type string.
String
is a value object, because with value objects you don't care which instance you are holding.
For example, when writing one a whiteboard you want to use a blue marker. If you have many markers which are blue, do you care which one you are holding?
If so, then that marker is an entity, if not it is a value object. Entities are equal when they have the same identity, value objects are equal when all properties that define one are equal.
Implementing Equality for Value Objects
To implement equality for a value object we need to compare each of its properties for equality (You could say that a value object's identity is defined by all of its properties). This is not hard, but it is repetitive work.
Each time you add a new property you have to update the Equals
method to use that property too.
The Microsoft Approach
There are implementations for a ValueObject
base class, which takes care of most of the work, for example, the one from Microsoft Docs.
Here you need to override the GetAtomicValues
method.
In case of Address
(copied from Docs):
protected override IEnumerable<object> GetAtomicValues()
{
// Using a yield return statement to return each element one at a time
yield return Street;
yield return City;
yield return State;
yield return Country;
yield return ZipCode;
}
Pretty simple, but I have two problems with this.
First of all, you need to inherit from the ValueObject
base class.
This excludes the use of Value Types as a Value Object.
Value types (struct
) are ideal Value Objects because they get embedded in the entities, just like built-in value objects int
and others.
The second objection is that you should not forget to add an extra property to this method each time you add a property to the type...
Using Reflection
So what is the solution? Of course, you could use reflection to implement Equals
like here.
In this case, reflection automatically discovers all the properties and compares all of them, returning true is all properties are equal.
The problem with reflection is that it is slow, so you should limit reflection to "just once".
"Just Once" Reflection
There is a third approach where you use reflection to figure out what to do and generate the code so the second time things go fast.
That is the approach I took to build ValueObjectComparer<T>
.
Here is an example of what a Value Object looks like.
The Equals
method simply delegates to the ValueObjectComparer<SomeObject>
.
Same for the IEquatable<SomeObject>
interface implementation.
public class SomeObject : IEquatable<SomeObject>
{
public string Name { get; set; }
public int Age { get; set; }
public override bool Equals(object obj)
=> ValueObjectComparer<SomeObject>.Instance.Equals(this, obj);
public bool Equals([AllowNull] SomeObject other)
=> ValueObjectComparer<SomeObject>.Instance.Equals(this, other);
}
Performance
Let's see how the performance compares between the 'Equals' as prescribed by Microsoft or the "Just Once" reflection implementation. For this I have used the excellent BenchmarkDotNet library, and here are the results:
| Method | Mean | Min | Max |
|------------------------------------------- |----------:|----------:|----------:|
| UsingMyValueObjectsThatAreEqual | 32.44 ms | 31.30 ms | 34.63 ms |
| UsingMyValueObjectsThatAreEqualWithNesting | 93.92 ms | 91.64 ms | 96.76 ms |
| UsingMyValueObjectsThatAreNotEqual | 29.57 ms | 28.89 ms | 30.45 ms |
| UsingMSValueObjectsThatAreEqual | 296.74 ms | 293.22 ms | 303.94 ms |
| UsingMSValueObjectsThatAreEqualWithNesting | 625.80 ms | 597.04 ms | 669.63 ms |
| UsingMSValueObjectsThatAreNotEqual | 248.53 ms | 238.87 ms | 261.64 ms |
| UsingMyValueObjectStructsThatAreEqual | 91.75 ms | 87.91 ms | 103.14 ms |
Using ValueObjectComparer<T>
results in about a 10x faster implementation.
Product | Versions 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. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.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. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on U2U.ValueObjectComparers:
Package | Downloads |
---|---|
U2U.BoardGames
Assembly containing a simple data model for use in training |
GitHub repositories
This package is not used by any popular GitHub repositories.
Use at your own risk ;)