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!

image

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!

6 Comments

  • In your impl of GetHashCode, doesn't the returned value change when the ID gets assigned by NH. IIRC hash codes that change are bad, for dictionaries etc.

    I just use the base class in Sharp Architecure :)

  • @alwin -- You are correct that GetHashCode changes when NHibernate assigns an Id (unless I'm assigning Guids or something similar), and this isn't the greatest result. However, Sharp Arch. only gets around this by having subclasses declare their "Signature Properties" which should be used in the hash code. If there are no signature properties, it just falls back on using the base.GetHashCode().

  • I prefer those Bailey Celine Sale's and happened to be anticipating these products a long time, to start with when i dressed in these products when i incidentally poured fluids for them nonetheless it became available, now any time when i dressed in these products on the list of those positions with it which had been Unattainable available, mending basically chicago along with proverb Celine Sale individuals messy readily and then the hosiery invariably stretch out on the foundation from my personal Celine Sale wen when i remove them "/ but additionally gives available in time so have down the sink one more very nearly 2 hundred so you can get one more twosome.

  • i prefer this neverwinter gold their excellent

  • logo plate to the center, the bag is looking really chic overall. Measurements: Width: 16.0 Inches Depth: 7.4 Inches Length of strap: 19.1 Inches Height: 12.5 Inches

  • asduhakjdhkjahdkjhaksdjlhasdjkdhahdkjhakhdkjahsjkdhjkssssssssssssssssssssssssssssssssssssssssss

Comments have been disabled for this content.