Base Class Object Equality for NHibernate Objects
In any project where you use an ORM you often have all of your domain classes inherit from a common base class. Among other things, your base class often contains your identity property. Mine has a protected IdT (this is the Id type) field called id, and a public getter called ID.
1: protected IdT id;
2:
3: public virtual IdT ID
4: {
5: get { return id; }
6: }
One important and complex question is how to handle equality of two domain objects. This becomes very important for caching, using HashSets, and more.
Before I share my solution I will mention that there are many implementations of base class equality out there, with S#arp Architecture’s standing out in my mind as a great though complex (maybe enterprise-level is a better word) variation.
Defining Equality
My idea of equality is that the base class will do its best to define equality that will work for most domain objects, but if a subclass would like to override Equals() then they are more than welcome.
Overall I would like domain object equality to use the following general criteria:
Two domain objects are equal if they are the same object (by reference).
If they are not the same object, one cannot be null, and they must be castable to each other [This one is tricky since we can’t use type equality because one might be a proxy type].
If the above criteria validate, then the Ids of the two objects must match but be non-default [So two transient object are always non-equal, and a persisted object can never equal a transient object].
Testing Equality Assumptions
Before I implement equals, I’ll write tests which verify all of my assumptions.
1: [TestClass]
2: public class EqualityTests
3: {
4: [TestMethod]
5: public void EqualsWithTwoNullObjectsReturnsTrue()
6: {
7: const SimpleDomainObject obj1 = null;
8: const SimpleDomainObject obj2 = null;
9:
10: var equality = Equals(obj1, obj2);
11:
12: Assert.AreEqual(true, equality);
13: }
14:
15: [TestMethod]
16: public void EqualsWithNullObjectReturnsFalse()
17: {
18: const SimpleDomainObject obj1 = null;
19: var obj2 = new SimpleDomainObject();
20:
21: var equality = Equals(obj1, obj2);
22:
23: Assert.AreEqual(false, equality);
24: }
25:
26: [TestMethod]
27: public void EqualsWithTransientObjectsReturnsFalse()
28: {
29: var obj1 = new SimpleDomainObject();
30: var obj2 = new SimpleDomainObject();
31:
32: var equality = Equals(obj1, obj2);
33:
34: Assert.AreEqual(false, equality);
35: }
36:
37: [TestMethod]
38: public void EqualsWithOneTransientObjectReturnsFalse()
39: {
40: var obj1 = new SimpleDomainObject();
41: var obj2 = new SimpleDomainObject();
42:
43: obj1.SetId(1);
44:
45: var equality = Equals(obj1, obj2);
46:
47: Assert.AreEqual(false, equality);
48: }
49:
50: [TestMethod]
51: public void EqualsWithDifferentIdsReturnsFalse()
52: {
53: var obj1 = new SimpleDomainObject();
54: var obj2 = new SimpleDomainObject();
55:
56: obj1.SetId(1);
57: obj2.SetId(2);
58:
59: var equality = Equals(obj1, obj2);
60:
61: Assert.AreEqual(false, equality);
62: }
63:
64: [TestMethod]
65: public void EqualsWithSameIdsReturnsTrue()
66: {
67: var obj1 = new SimpleDomainObject();
68: var obj2 = new SimpleDomainObject();
69:
70: obj1.SetId(1);
71: obj2.SetId(1);
72:
73: var equality = Equals(obj1, obj2);
74:
75: Assert.AreEqual(true, equality);
76: }
77:
78: [TestMethod]
79: public void EqualsWithSameIdsInSubclassReturnsTrue()
80: {
81: var obj1 = new SimpleDomainObject();
82: var obj2 = new SubSimpleDomainObject();
83:
84: obj1.SetId(1);
85: obj2.SetId(1);
86:
87: var equality = Equals(obj1, obj2);
88:
89: Assert.AreEqual(true, equality);
90: }
91:
92: [TestMethod]
93: public void EqualsWithDifferentIdsInDisparateClassesReturnsFalse()
94: {
95: var obj1 = new SimpleDomainObject();
96: var obj2 = new OtherSimpleDomainObject();
97:
98: obj1.SetId(1);
99: obj2.SetId(2);
100:
101: var equality = Equals(obj1, obj2);
102:
103: Assert.AreEqual(false, equality);
104: }
105:
106: [TestMethod]
107: public void EqualsWithSameIdsInDisparateClassesReturnsFalse()
108: {
109: var obj1 = new SimpleDomainObject();
110: var obj2 = new OtherSimpleDomainObject();
111:
112: obj1.SetId(1);
113: obj2.SetId(1);
114:
115: var equality = Equals(obj1, obj2);
116:
117: Assert.AreEqual(false, equality);
118: }
119: }
120:
121: public class SimpleDomainObject : DomainObject<SimpleDomainObject,int>
122: {
123: public void SetId(int ident)
124: {
125: id = ident;
126: }
127: }
128:
129: public class SubSimpleDomainObject : SimpleDomainObject{}
130:
131: public class OtherSimpleDomainObject : DomainObject<OtherSimpleDomainObject,int>
132: {
133: public void SetId(int ident)
134: {
135: id = ident;
136: }
137: }
This is a lot of code but if you look at each test by itself you’ll see that if all the tests pass I will have equality implemented correctly, or at least correctly as far as I defined it above.
If you run the tests now you will see that the tests fail when you are trying to test equality as two objects having the same Ids. Let’s implement an Equals() method and see what we can come up with.
Implementing Equals
This implementation is based off of an existing DomainObject<T,IdT> base class that is similar to the one proposed in the NHibernate Best Practices article.
1: public override bool Equals(object other)
2: {
3: if (ReferenceEquals(this, other)) return true;
4:
5: if (other == null || other is T == false) return false; //is returns true if other is castable to T
6:
7: return Equals(other as DomainObject<T, IdT>);
8: }
9:
10: private bool Equals(DomainObject<T, IdT> other)
11: {
12: if (ReferenceEquals(null, other)) return false;
13:
14: //Domain objects are equal if their ids are equal and non-default
15: if (Equals(id, default(IdT))) return false;
16:
17: return Equals(id, other.id);
18: }
Now if we run the tests we get all greens!
Getting the HashCode
Whenever you override Equals(), it is recommended that you override GetHashCode(). In our case this is pretty easy, since we just want to use the id’s HashCode (or the base HashCode if id is null/default).
UPDATE: Modified GetHashCode() to include the base class’ hash code so we get less collisions between disparate classes.
1: public override int GetHashCode()
2: {
3: return Equals(id, default(IdT)) ? base.GetHashCode() : (base.GetHashCode() * 31) + id.GetHashCode();
4: }
1: public override int GetHashCode()
2: {
3: return Equals(id, default(IdT)) ? base.GetHashCode() : id.GetHashCode();
4: }
Enjoy!