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</ul> 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</ul> Additionally, Framework Design Guidelines has some additional requirements I must meet:
-
Provide a reflexive, transitive, and symmetric implementation of Equals
- Override GetHashCode
- Implement IEquatable
- Override the equality operators</ul>
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);
}
}
- Override the equality operators</ul>
- Implement IEquatable
- Override GetHashCode
-
-
</pre> </div>
I borrowed a little bit from the .NET [ValueType](http://msdn2.microsoft.com/en-us/library/system.valuetype.aspx) 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](http://www.nunit.org/) tests I used to write this class up. I think the tests describe the intended behavior well enough.
<div class="CodeFormatContainer">
<pre>[TestFixture]<br /> <span class="kwrd">public</span> <span class="kwrd">class</span> ValueObjectTests<br /> {<br /> <span class="kwrd">private</span> <span class="kwrd">class</span> Address : ValueObject<Address><br /> {<br /> <span class="kwrd">private</span> <span class="kwrd">readonly</span> <span class="kwrd">string</span> _address1;<br /> <span class="kwrd">private</span> <span class="kwrd">readonly</span> <span class="kwrd">string</span> _city;<br /> <span class="kwrd">private</span> <span class="kwrd">readonly</span> <span class="kwrd">string</span> _state;<br /> <br /> <span class="kwrd">public</span> Address(<span class="kwrd">string</span> address1, <span class="kwrd">string</span> city, <span class="kwrd">string</span> state)<br /> {<br /> _address1 = address1;<br /> _city = city;<br /> _state = state;<br /> }<br /> <br /> <span class="kwrd">public</span> <span class="kwrd">string</span> Address1<br /> {<br /> get { <span class="kwrd">return</span> _address1; }<br /> }<br /> <br /> <span class="kwrd">public</span> <span class="kwrd">string</span> City<br /> {<br /> get { <span class="kwrd">return</span> _city; }<br /> }<br /> <br /> <span class="kwrd">public</span> <span class="kwrd">string</span> State<br /> {<br /> get { <span class="kwrd">return</span> _state; }<br /> }<br /> }<br /> <br /> <span class="kwrd">private</span> <span class="kwrd">class</span> ExpandedAddress : Address<br /> {<br /> <span class="kwrd">private</span> <span class="kwrd">readonly</span> <span class="kwrd">string</span> _address2;<br /> <br /> <span class="kwrd">public</span> ExpandedAddress(<span class="kwrd">string</span> address1, <span class="kwrd">string</span> address2, <span class="kwrd">string</span> city, <span class="kwrd">string</span> state)<br /> : <span class="kwrd">base</span>(address1, city, state)<br /> {<br /> _address2 = address2;<br /> }<br /> <br /> <span class="kwrd">public</span> <span class="kwrd">string</span> Address2<br /> {<br /> get { <span class="kwrd">return</span> _address2; }<br /> }<br /> <br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> AddressEqualsWorksWithIdenticalAddresses()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.IsTrue(address.Equals(address2));<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> AddressEqualsWorksWithNonIdenticalAddresses()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"Address2"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.IsFalse(address.Equals(address2));<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> AddressEqualsWorksWithNulls()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="kwrd">null</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"Address2"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.IsFalse(address.Equals(address2));<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> AddressEqualsWorksWithNullsOnOtherObject()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address2"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"Address2"</span>, <span class="kwrd">null</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.IsFalse(address.Equals(address2));<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> AddressEqualsIsReflexive()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.IsTrue(address.Equals(address));<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> AddressEqualsIsSymmetric()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"Address2"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.IsFalse(address.Equals(address2));<br /> Assert.IsFalse(address2.Equals(address));<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> AddressEqualsIsTransitive()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address3 = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.IsTrue(address.Equals(address2));<br /> Assert.IsTrue(address2.Equals(address3));<br /> Assert.IsTrue(address.Equals(address3));<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> AddressOperatorsWork()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address3 = <span class="kwrd">new</span> Address(<span class="str">"Address2"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.IsTrue(address == address2);<br /> Assert.IsTrue(address2 != address3);<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> DerivedTypesBehaveCorrectly()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> ExpandedAddress address2 = <span class="kwrd">new</span> ExpandedAddress(<span class="str">"Address1"</span>, <span class="str">"Apt 123"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.IsFalse(address.Equals(address2));<br /> Assert.IsFalse(address == address2);<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> EqualValueObjectsHaveSameHashCode()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.AreEqual(address.GetHashCode(), address2.GetHashCode());<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> TransposedValuesGiveDifferentHashCodes()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="kwrd">null</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"TX"</span>, <span class="str">"Austin"</span>, <span class="kwrd">null</span>);<br /> <br /> Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> UnequalValueObjectsHaveDifferentHashCodes()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"Address1"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="str">"Address2"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> TransposedValuesOfFieldNamesGivesDifferentHashCodes()<br /> {<br /> Address address = <span class="kwrd">new</span> Address(<span class="str">"_city"</span>, <span class="kwrd">null</span>, <span class="kwrd">null</span>);<br /> Address address2 = <span class="kwrd">new</span> Address(<span class="kwrd">null</span>, <span class="str">"_address1"</span>, <span class="kwrd">null</span>);<br /> <br /> Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());<br /> }<br /> <br /> [Test]<br /> <span class="kwrd">public</span> <span class="kwrd">void</span> DerivedTypesHashCodesBehaveCorrectly()<br /> {<br /> ExpandedAddress address = <span class="kwrd">new</span> ExpandedAddress(<span class="str">"Address99999"</span>, <span class="str">"Apt 123"</span>, <span class="str">"New Orleans"</span>, <span class="str">"LA"</span>);<br /> ExpandedAddress address2 = <span class="kwrd">new</span> ExpandedAddress(<span class="str">"Address1"</span>, <span class="str">"Apt 123"</span>, <span class="str">"Austin"</span>, <span class="str">"TX"</span>);<br /> <br /> Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());<br /> }<br /> <br /> }<br />
</pre> </div>
#### **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</ul>
* Fixed NullReferenceException bug when “other” is null for IEquatable<T>.Equals
* Added tests to cover bugs and fixes</ul>