Generic Value Object Equality

This post was originally published here.

I read a post from Oren the other day where he posted some code for a generic Entity base type that implemented the correct equality logic.  I realized that I’ve needed a generic base type for Value Objects as well.

Value Object Requirements

In the Domain Driven Design space, a Value Object:

  • Has no concept of an identity
    • Two different instances of a Value Object with the same values are considered equal
  • Describes a characteristic of another thing
  • Is immutable

Unfortunately, in nearly all cases I’ve run in to, we can’t use Value Types in .NET to represent Value Objects.  Value Types (struct) have some size limitations (~16 bytes or less), which we run into pretty quickly.  Instead, we can create a Reference Type (class) with Value Type semantics, similar to the .NET String type.  The String type is a Reference Type, but exhibits Value Type semantics, since it is immutable.  For a Reference Type to exhibit Value Type semantics, it must:

  • Be immutable
  • Override the Equals method, to implement equality instead of identity, which is the default

Additionally, Framework Design Guidelines has some additional requirements I must meet:

  • Provide a reflexive, transitive, and symmetric implementation of Equals
  • Override GetHashCode
  • Implement IEquatable<T>
  • Override the equality operators

Generic Implementation

What I wanted was a base class that would give me all of the Framework Design Guidelines requirements as well as the Domain Driven Design requirements, without any additional logic from concrete types.  Here’s what I ended up with:

public abstract class ValueObject<T> : IEquatable<T>
    where T : ValueObject<T>
{
    public override bool Equals(object obj)
    {
        if (obj == null)
            return false;

        T other = obj as T;

        return Equals(other);
    }

    public override int GetHashCode()
    {
        IEnumerable<FieldInfo> fields = GetFields();

        int startValue = 17;
        int multiplier = 59;

        int hashCode = startValue;

        foreach (FieldInfo field in fields)
        {
            object value = field.GetValue(this);

            if (value != null)
                hashCode = hashCode * multiplier + value.GetHashCode();
        }

        return hashCode;
    }

    public virtual bool Equals(T other)
    {
        if (other == null)
            return false;

        Type t = GetType();
        Type otherType = other.GetType();

        if (t != otherType)
            return false;

        FieldInfo[] fields = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

        foreach (FieldInfo field in fields)
        {
            object value1 = field.GetValue(other);
            object value2 = field.GetValue(this);

            if (value1 == null)
            {
                if (value2 != null)
                    return false;
            }
            else if (! value1.Equals(value2))
                return false;
        }

        return true;
    }

    private IEnumerable<FieldInfo> GetFields()
    {
        Type t = GetType();

        List<FieldInfo> fields = new List<FieldInfo>();

        while (t != typeof(object))
        {
            fields.AddRange(t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public));

            t = t.BaseType;
        }

        return fields;
    }

    public static bool operator ==(ValueObject<T> x, ValueObject<T> y)
    {
        return x.Equals(y);
    }

    public static bool operator !=(ValueObject<T> x, ValueObject<T> y)
    {
        return ! (x == y);
    }
}

I borrowed a little bit from the .NET ValueType base class for the implementation of Equals.  The ValueObject<T> type uses reflection to access and compare all internal fields for Equals, as well as for GetHashCode.  All implementers will need to do is to ensure that their concrete type is immutable, and they’re done.

I could probably optimize the reflection calls and cache them, but this implementation is mainly for reference anyway.

The Tests

Just for completeness, I’ll include the set of NUnit tests I used to write this class up.  I think the tests describe the intended behavior well enough.

[TestFixture]
public class ValueObjectTests
{
    private class Address : ValueObject<Address>
    {
        private readonly string _address1;
        private readonly string _city;
        private readonly string _state;

        public Address(string address1, string city, string state)
        {
            _address1 = address1;
            _city = city;
            _state = state;
        }

        public string Address1
        {
            get { return _address1; }
        }

        public string City
        {
            get { return _city; }
        }

        public string State
        {
            get { return _state; }
        }
    }

    private class ExpandedAddress : Address
    {
        private readonly string _address2;

        public ExpandedAddress(string address1, string address2, string city, string state)
            : base(address1, city, state)
        {
            _address2 = address2;
        }

        public string Address2
        {
            get { return _address2; }
        }

    }

    [Test]
    public void AddressEqualsWorksWithIdenticalAddresses()
    {
        Address address = new Address("Address1", "Austin", "TX");
        Address address2 = new Address("Address1", "Austin", "TX");

        Assert.IsTrue(address.Equals(address2));
    }

    [Test]
    public void AddressEqualsWorksWithNonIdenticalAddresses()
    {
        Address address = new Address("Address1", "Austin", "TX");
        Address address2 = new Address("Address2", "Austin", "TX");

        Assert.IsFalse(address.Equals(address2));
    }

    [Test]
    public void AddressEqualsWorksWithNulls()
    {
        Address address = new Address(null, "Austin", "TX");
        Address address2 = new Address("Address2", "Austin", "TX");

        Assert.IsFalse(address.Equals(address2));
    }

    [Test]
    public void AddressEqualsWorksWithNullsOnOtherObject()
    {
        Address address = new Address("Address2", "Austin", "TX");
        Address address2 = new Address("Address2", null, "TX");

        Assert.IsFalse(address.Equals(address2));
    }
    
    [Test]
    public void AddressEqualsIsReflexive()
    {
        Address address = new Address("Address1", "Austin", "TX");

        Assert.IsTrue(address.Equals(address));
    }

    [Test]
    public void AddressEqualsIsSymmetric()
    {
        Address address = new Address("Address1", "Austin", "TX");
        Address address2 = new Address("Address2", "Austin", "TX");

        Assert.IsFalse(address.Equals(address2));
        Assert.IsFalse(address2.Equals(address));
    }

    [Test]
    public void AddressEqualsIsTransitive()
    {
        Address address = new Address("Address1", "Austin", "TX");
        Address address2 = new Address("Address1", "Austin", "TX");
        Address address3 = new Address("Address1", "Austin", "TX");

        Assert.IsTrue(address.Equals(address2));
        Assert.IsTrue(address2.Equals(address3));
        Assert.IsTrue(address.Equals(address3));
    }

    [Test]
    public void AddressOperatorsWork()
    {
        Address address = new Address("Address1", "Austin", "TX");
        Address address2 = new Address("Address1", "Austin", "TX");
        Address address3 = new Address("Address2", "Austin", "TX");

        Assert.IsTrue(address == address2);
        Assert.IsTrue(address2 != address3);
    }

    [Test]
    public void DerivedTypesBehaveCorrectly()
    {
        Address address = new Address("Address1", "Austin", "TX");
        ExpandedAddress address2 = new ExpandedAddress("Address1", "Apt 123", "Austin", "TX");

        Assert.IsFalse(address.Equals(address2));
        Assert.IsFalse(address == address2);
    }

    [Test]
    public void EqualValueObjectsHaveSameHashCode()
    {
        Address address = new Address("Address1", "Austin", "TX");
        Address address2 = new Address("Address1", "Austin", "TX");

        Assert.AreEqual(address.GetHashCode(), address2.GetHashCode());
    }

    [Test]
    public void TransposedValuesGiveDifferentHashCodes()
    {
        Address address = new Address(null, "Austin", "TX");
        Address address2 = new Address("TX", "Austin", null);

        Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());
    }

    [Test]
    public void UnequalValueObjectsHaveDifferentHashCodes()
    {
        Address address = new Address("Address1", "Austin", "TX");
        Address address2 = new Address("Address2", "Austin", "TX");

        Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());
    }

    [Test]
    public void TransposedValuesOfFieldNamesGivesDifferentHashCodes()
    {
        Address address = new Address("_city", null, null);
        Address address2 = new Address(null, "_address1", null);

        Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());
    }

    [Test]
    public void DerivedTypesHashCodesBehaveCorrectly()
    {
        ExpandedAddress address = new ExpandedAddress("Address99999", "Apt 123", "New Orleans", "LA");
        ExpandedAddress address2 = new ExpandedAddress("Address1", "Apt 123", "Austin", "TX");

        Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());
    }

}

UPDATE: 6/27/07

  • Changed ValueObject<T> to implement IEquatable<T> instead of IEquatable<ValueObject<T>>
    • Equals reflects derived type instead of base type, since C# generics are not covariant (or contravariant), IEquatable<ValueObject<T>> != IEquatable<T>
  • Changed GetHashCode algorithm to use a calculated hash to cover additional test cases
    • Gather parent type field values
    • Fix transposed value bug
  • Fixed NullReferenceException bug when “other” is null for IEquatable<T>.Equals
  • Added tests to cover bugs and fixes

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Jimmy Bogard

I'm a technical architect with Headspring in Austin, TX. I focus on DDD, distributed systems, and any other acronym-centric design/architecture/methodology. I created AutoMapper and am a co-author of the ASP.NET MVC in Action books.
This entry was posted in C#, Domain Driven Design. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Bokkas Akash

    Have you tried to test for null? I guess, it is failing for null
    Address addr = null;
    if(addr == null) return “matched”;
    OR
    if(addr != null) return “matched”;

  • Pingback: Comparing Value Objects and Null