Skip to main content

A Value Object cannot live on its own without an Entity. An object that represents a descriptive aspect of the domain with no conceptual identity is called a VALUE OBJECT. VALUE OBJECTS are instantiated to represent elements of the design that we care about only for what they are, not who or which they are. The following is a C# base class that gives all the Framework Design Guidelines requirements, as well as the Domain Driven Design requirements, without any additional logic from concrete types.

using System;
using System.Collections.Generic;
using System.Reflection;

// ============================================================================
// Generic Value Object
//
// ## 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:
//
// Author: Jimmy Bogard
// http://grabbagoft.blogspot.com/2007/06/generic-value-object-equality.html
// https://lostechies.com/joeocampo/2007/04/23/a-discussion-on-domain-driven-design-value-objects/
// ============================================================================

public abstract class ValueObject<T> : IEquatable<T> where T : ValueObject<T>
{
    public virtual bool Equals(T other)
    {
        if (other == null)
        {
            return false;
        }

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

        if (t != otherType)
        {
            return false;
        }

        var fields = t.GetFields(
                         BindingFlags.Instance |
                         BindingFlags.NonPublic |
                         BindingFlags.Public);

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

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

        return true;
    }

    public override bool Equals(object obj)
    {
        if (obj == null)
        {
            return false;
        }

        var other = obj as T;

        return Equals(other);
    }

    public override int GetHashCode()
    {
        var fields = GetFields();

        var startValue = 17;
        var multiplier = 59;

        var hashCode = startValue;

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

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

        return hashCode;
    }

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

        var 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);
    }
}

// ============================================================================
// 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());
    }

}