It’s what’s inside that counts.

The default comparison for equality in .Net, when comparing reference types, uses reference equality. The == operator compares the two class references and if they are the same instance (have a pointer to the same address in memory), returns true. Otherwise false.

In most of my day-to-day, however, I’d prefer to use value equality. That’s comparing the values of the properties of 2 instances of a class to determine equality.

Actually, I’d like to go one step further. I don’t want to compare all the properties of the class instances, just the ones that I determine indicate identity. So I whipped up a little interface IStructurallyIdentifiable<T> to help us out towards this goal. Implementers of this interface determine which properties determine identity for the class.

It is VERY important that the implementing class override Equals and GetHashCode. The compiler will force you to implement Equals<T>, and that will be enough for one-to-one comparisons, but if you want to use LINQ to Objects to handle equality comparisons of collections (think .Union, .Intersect, and .Except), you’ll need to override the implementations of .Equals and .GetHashCode. See the Phone class for an example.

public interface IStructurallyIdentifiable<T> : IEquatable<T>
{
    IEnumerable<Expression<Func<T, object>>> GetIdentityProperties();
}
public class Phone : IStructurallyIdentifiable<Phone>
{
    // Values that determine identity for type Phone.
    public string TypeCode { get; set; }
    public string Number { get; set; }
    
    // Audit properties, not used to determine identity
    public DateTime CreatedAt { get; set; }
    public int CreatedBy { get; set; }
    public DateTime ModifiedAt { get; set; }
    public int ModifiedBy { get; set; }
    
    public static Phone Create(string typeCode, string number)
    {
        return new Phone
        {
            TypeCode = typeCode,
            Number = number,
        };
    }

    public IEnumerable<Expression<Func<Phone, object>>> GetIdentityProperties()
    {
        yield return x => x.TypeCode;
        yield return x => x.Number;
    }

    public bool Equals(Phone other) => this.EqualsImpl(other);
    public override bool Equals(object obj) => this.EqualsImpl(obj);
    public override int GetHashCode() => this.GetHashCodeImpl();

    public override string ToString() => $"{TypeCode} => {Number}";
}
public static class StructurallyIdentifiableExtensions
{
    public static bool EqualsImpl<T>(this T self, object other)
        where T : IStructurallyIdentifiable<T>
    {
        if (self == null && other == null ) { return true; }
        if (self == null || other == null) { return false; }
        var t = typeof(T);
        foreach (var accessor in self.GetIdentityProperties())
        {
            var memberName = StaticReflection.GetMemberName(accessor);
            var property = t.GetProperty(memberName);
            if (!Equals(property.GetValue(self), property.GetValue(other))) { return false; }
        }

        return true;
    }

    /// <summary>
    /// http://stackoverflow.com/a/263416/237012
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="self"></param>
    /// <returns></returns>
    public static int GetHashCodeImpl<T>(this T self)
       where T : IStructurallyIdentifiable<T>
    {
        unchecked // Overflow is fine, just wrap
        {
            int hash = (int)2166136261;

            foreach (var accessor in self.GetIdentityProperties())
            {
                var value = accessor.Compile()(self);

                if (value != null)
                {
                    hash = hash * 16777619 ^ value.GetHashCode();
                }
            }
            return hash;
        }
    }
}

One last note - if you’re looking for an implementation of the StaticReflection class, it’s in the following snippet, copied from Joel Abrahamsson’s blog.

/// <summary>
/// http://joelabrahamsson.com/getting-property-and-method-names-using-static-reflection-in-c/
/// </summary>
public static class StaticReflection
{
    public static string GetMemberName<T, TValue>(
        this T instance,
        Expression<Func<T, TValue>> expression)
    {
        return GetMemberName(expression);
    }

    public static string GetMemberName<T, TValue>(
        Expression<Func<T, TValue>> expression)
    {
        if (expression == null)
        {
            throw new ArgumentException(
                "The expression cannot be null.");
        }

        return GetMemberName(expression.Body);
    }

    public static string GetMemberName<T>(
        this T instance,
        Expression<Action<T>> expression)
    {
        return GetMemberName(expression);
    }

    public static string GetMemberName<T>(
        Expression<Action<T>> expression)
    {
        if (expression == null)
        {
            throw new ArgumentException(
                "The expression cannot be null.");
        }

        return GetMemberName(expression.Body);
    }

    private static string GetMemberName(
        Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentException(
                "The expression cannot be null.");
        }

        if (expression is MemberExpression)
        {
            // Reference type property or field
            var memberExpression =
                (MemberExpression)expression;
            return memberExpression.Member.Name;
        }

        if (expression is MethodCallExpression)
        {
            // Reference type method
            var methodCallExpression =
                (MethodCallExpression)expression;
            return methodCallExpression.Method.Name;
        }

        if (expression is UnaryExpression)
        {
            // Property, field of method returning value type
            var unaryExpression = (UnaryExpression)expression;
            return GetMemberName(unaryExpression);
        }

        throw new ArgumentException("Invalid expression");
    }

    private static string GetMemberName(
        UnaryExpression unaryExpression)
    {
        if (unaryExpression.Operand is MethodCallExpression)
        {
            var methodExpression =
                (MethodCallExpression)unaryExpression.Operand;
            return methodExpression.Method.Name;
        }

        return ((MemberExpression)unaryExpression.Operand)
            .Member.Name;
    }
}

Thanks for reading! If you have any suggestions for improvements, please add a comment or hit me up on Twitter!