In the first part, I showed you how you can implement the Equals
method for Value Objects efficiently and easily by using the U2U.ValueObjectComparers
NuGet package.
In this blog post, I want to show you how this is implemented.
Implementation Recap
Let's say you have a Value Object, and you want to implement Equals
.
You need to override the Equals
method from object
, and also the IEquatable<T>
interface:
public class SomeObject : IEquatable<SomeObject>
{
public static bool operator ==(SomeObject left, SomeObject right)
=> ValueObjectComparer<SomeObject>.Instance.Equals(this, other);
public static bool operator !=(SomeObject left, SomeObject right)
=> !(left == right);
public string Name { get; set; }
public int Age { get; set; }
public override bool Equals([AllowNull] object obj)
=> ValueObjectComparer<SomeObject>.Instance.Equals(this, obj);
public bool Equals([AllowNull] SomeObject other)
=> ValueObjectComparer<SomeObject>.Instance.Equals(this, other);
}
How Does it Work?
Let's start with the ValueObjectComparer<T>
class. The Equals
method which is overridden from the object
base class checks if the references are equal. If so, we can return true
immediately. Then we check the right
instance and if it is null
or of the wrong type we return false. This will also return false
if the left instance is null
.
Now we know that both left and right are non-null instances of T
, and we call the comparer
. this comparer
is constructed using "Just Once" reflection for type T
.
public sealed class ValueObjectComparer<T> where T : class
{
public static ValueObjectComparer<T> Instance { get; } = new ValueObjectComparer<T>();
private CompFunc<T> comparer = ExpressionGenerater.GenerateComparer<T>();
public bool Equals(T left, T right)
=> object.ReferenceEquals(left, right)
|| (left is object && right is object && this.comparer(left, right));
public bool Equals(T left, object? right)
=> object.ReferenceEquals(left, right)
|| (right is object && right.GetType() == left?.GetType()
&& this.comparer(left, (T)right));
}
Constructing Comparer
The comparer is constructed by calling the ExpressionGenerator.GenerateComparer<T>
method.
private CompFunc<T> comparer = ExpressionGenerater.GenerateComparer<T>();
internal static CompFunc<T> GenerateComparer<T>()
{
List<Expression> comparers = new List<Expression>();
ParameterExpression left = Expression.Parameter(typeof(T), "left");
ParameterExpression right = Expression.Parameter(typeof(T), "right");
foreach (PropertyInfo propInfo in typeof(T)
.GetProperties(BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public))
{
if( propInfo.IsDefined(typeof(IgnoreAttribute)))
{
continue;
}
comparers.Add(GenerateEqualityExpression(left, right, propInfo));
}
Expression ands = comparers.Aggregate((left, right) => Expression.AndAlso(left, right));
var andComparer = Expression.Lambda<CompFunc<T>>(ands, left, right).Compile();
return andComparer;
}
This method uses reflection to walk over each property of the type. If the property has the IgnoreAttribute
it skips over to the next one.
Otherwise, it generates a comparer Expression
for that property, discussed in the next section.
When all properties have been iterated, all these expressions are combined in one big and
expression, which is compiled into a Func<T, T, bool>
.
This expression looks a bit like this (simplified):
(this.FirstName is object && this.FirstName.Equals(other.FirstName)
&& (this.LastName is object && this.LastName.Equals(other.LastName )
&& this.Age.Equals(other.Age)
&& (this.Nested is object && this.Nested .Equals(other.Nested )
Generating the Expression
The GenerateEqualityExpression
method returns with an Expression
to check for equality of a certain property. Either the property is a reference or a value type, and in both cases, the property type can implement IEquality<T>
.
First, we check for the existence of the IEquality<T>
interface on the property's type. This is used to decide which Equals
we will call.
private static Expression GenerateEqualityExpression(ParameterExpression left,
ParameterExpression right,
PropertyInfo prop)
{
Type propertyType = prop.PropertyType;
Type equitableType = typeof(IEquatable<>).MakeGenericType(propertyType);
MethodInfo equalMethod;
Expression equalCall;
if (equitableType.IsAssignableFrom(propertyType))
{
equalMethod = equitableType.GetMethod(nameof(Equals), new Type[] { propertyType });
equalCall = Expression.Call(Expression.Property(left, prop),
equalMethod,
Expression.Property(right, prop));
}
else
{
equalMethod = propertyType.GetMethod(nameof(Equals), new Type[] { typeof(object) });
equalCall = Expression.Call(Expression.Property(left, prop),
equalMethod,
Expression.Convert(Expression.Property(right, prop),
typeof(object)));
}
equalCall
contains the expression to call Equals
on the instance.
Now we can call Equals
, but we still need to check if the property's type is a value type. Because then we don't need to check for null
references. Otherwise, the property is a reference type, so we check for reference equality first. If the references are not equal, then we will call the left
property value's Equals
, but only of left
is not null
...
This dynamically generates for reference types the following expression:
Expression<Func<T, T, bool>> either = (T x, T y)
=> object.ReferenceEquals(x, y) || (x != null && x.Equals(y));
if (prop.PropertyType.IsValueType)
{
return equalCall;
}
else
{
Expression leftValue = Expression.Property(left, prop);
Expression rightValue = Expression.Property(right, prop);
Expression refEqual = Expression.ReferenceEqual(leftValue, rightValue);
Expression nullConst = Expression.Constant(null);
Expression leftIsNotNull = Expression.Not(Expression.ReferenceEqual(leftValue, nullConst));
Expression leftIsNotNullAndIsEqual = Expression.AndAlso(leftIsNotNull, equalCall);
Expression either = Expression.OrElse(refEqual, leftIsNotNullAndIsEqual);
return either;
}
The implementation for GetHashCode
is covered in the next blog post.
Sources Please?
All sources can be found in my GitHub repository.