Monday, June 25, 2007

Generic Value Object Equality

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

4 comments:

Colin Jack said...

Looks good, haven't tried it out yet but I definitely like it.

I also find structs aren't so useful for value objects in .NET but to be honest I often find it hard to completely fulfill the:

"Has no concept of an identity"

Constraint especially when the domain value objects are saved outside the table of the owning entity. Its not a big problem but it does irritate me a little.

Anonymous said...

There is a problem with GetHashCode function, i have problem of stack overflow...

Unknown said...

Since it isn't a true value type what happens if the object is null. For example:

if (object != null)
// Do something

I believe it will ultimately return true and throw and NullRef exception when you use it.

Jimmy Bogard said...

@Cory

Yes, that's a bug, thanks! It should use Equals(object, null).