Implementing ValueObject's Equality - Efficiently - Part 2

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.

/Equality2.jpg

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.