In .NET, two objects are supposed to be equal when they both point to the same object in memory. This is also called reference equality. This is what the Equals
method on System.Object
implements. Reference equality works for normal .NET objects but is not adequate for persistent entities. Two persistent entities can be said to be equal if they point to the same database record. So if you have loaded two instances of same employee record then they are not equal on reference equality measures, but for NHibernate they are equal.
Equality obviously matters if you want to compare two entities. But beyond the basics, equality matters because some basic collection operations depend on equality of objects. Calling the Add
or Remove
methods on ICollection
in order to add or remove items to/from collection, internally depends on the equality check of the item being added. This is more important during removal of an item because some kind of equality check has to happen before the correct item to be removed is determined. For add, as long as the collection allows duplicates, no equality checks are required. For a collection represented using sets/maps which does not allow duplicates, every time a new item is added to the collection, equality check happens. Fortunately, NHibernate handles situations like these quite well if the entities involved are persistent entities loaded using the same session. But following are two situations where NHibernate cannot handle the equality and defers to .NET's implementation of equals (which defers to reference equality):
int
, guid
is what NHibernate can handle for equality checks.Let's see with the following example how absence of proper equality checks may fail some operations. The code is written in the form of a unit test. We would then implement equality to make this test pass.
[Test] public void SameEntityLoadedFromTwoDifferentSessionsMatches() { var config = new DatabaseConfigurationForSqlServer(); object id = 0; var employee1 = new Employee { EmployeeNumber = "123456789", DateOfBirth = new DateTime(1980, 2, 23), DateOfJoining = new DateTime(2012, 3, 15) }; using (var session = config.OpenSession()) { using (var transaction = session.BeginTransaction()) { id = session.Save(employee1); transaction.Commit(); } } Employee employee2 = null; using (var session = config.OpenSession()) { using (var transaction = session.BeginTransaction()) { employee2 = session.Get<Employee>(id); transaction.Commit(); } } Assert.That(employee1.Equals(employee2)); }
In the preceding test, we have saved an instance of the Employee
entity named employee1
. This operation is carried out in a session and that session is then disposed. We then open a new session and load the same employee instance from database and assign it to a different local variable named employee2
this time. In the end, we compare the two instances using the Equals
method. If you run the preceding test, it would fail saying employee1
and employee2
do not match. This happens because in absence of our custom equality checks, CLR defers to reference equality checks and being two different .NET objects, employee1
and emplpoyee2
fail on that count.
In the preceding test, we have used a class named DatabaseConfigurationForSqlServer
. This class builds NHibernate configuration against a SQL Server 2012 instance. We have been using SQLite for tests and that is the preferred database to use for tests. But SQLite has a limitation when used in in-memory mode; it does not support multiple sessions connecting to same database instance. Multiple sessions connecting to same database instance are exactly the thing we wanted for this test and hence I had to use SQL Server.
Next, we will see how to implement the custom equality checks that compare two persistent entities and consider them to be equal if they both point to the same database record.
Two entities can be said to be equal if they both correspond to the same database record. In order to implement such a notion of equality for entities, we would need to override the following two methods on all our entities:
public int GetHashCode() public bool Equals(object obj)
These methods are defined as virtual
methods on System.Object
. Before we implement these methods, let's take a moment and think about what we want to implement here. First and foremost, we want to make sure that the Id
properties of the two entity instances have same value. We can extend the same logic for implementation of the GetHashCode()
method. We can return hashcode of the Id
property from our implementation of the GetHashCode
method. But what if the entity is transient and does not have any id set? Since transient entities do not correspond to any database records, we can resort to .NET's reference equality check. So, we would use reference equality if entity is transient, else we would compare the identifier values of the two entity instances. That would give us the following implementation of these two methods:
public override int GetHashCode() { return Id.GetHashCode(); } public override bool Equals(object obj) { var thisIsTransient = Id == 0; var otherIsTransient = other.Id == 0; if (thisIsTransient && otherIsTransient) return ReferenceEquals(this, other); return Id == other.Id; }
There are two problems with the preceding code:
Equals
method could be of any type. Even worse, it could be a null. We need to safeguard our code against such situations.GetHashCode
always returns hash code of the Id
property. When a transient entity is saved and it is assigned an ID, the hashcode returned by this method would change. That is a violation of the GetHashCode
implementation contract. Hashcode once assigned to an object must not be changed.To fix the first problem, we just need to typecast the object passed into the Equals
method to correct type and also check that it is not null. The second problem is more challenging to fix. We would need to generate hashcode for entity when it is transient and cache it. For this, we can fall back to .NET's default implementation of the GetHashCode
method. Once the hashcode is generated and cached, we can return the same hashcode even when the entity becomes persistent. And if entity is already persistent (an entity loaded from database), we would cache and return the hashcode of its identifier. Following code listing shows this implementation:
private int? hashCode; public override int GetHashCode() { if (hashCode.HasValue) return hashCode.Value; var transientEntity = Id == 0; if (transientEntity) { hashCode = base.GetHashCode(); return hashCode.Value; } return Id.GetHashCode(); }
Note the use of nullable int
type to cache the hashcode. Another thing to note is that the preceding code uses a value of 0
for the identifier property to say that the entity is transient. This works for identifier strategies such as identity/hilo but would not work for other strategies such as guid or guidcomb, which do not generate an integer type of identifier value. Also, we have ignored a setting called unsaved-value
declared during the mapping of identifier properties to specify the identifier value for transient entities. If this setting is declared to be other than 0
then preceding code needs to change accordingly.
The above implementation still has a minor flaw. If the identifier of an entity changes then the GetHashCode
contract is violated because the method now starts returning a different hashcode. While it is easy to cache the hashcode generated by calling Id.GetHashCode()
, I would advise against it. I would rather suggest to try and not set identifier of a persistent entity to a different value. This is not ideal to do and can lead to subtle bugs.
In the code samples of this chapter, you will have seen that I have done additional refactoring of the above code. Because every entity needs implementation of equality, I moved these two methods into the EntityBase
class. But then I could not anymore check the type of the object passed into the Equals
method. To enable the type checking, I added a generic parameter on EntityBase
to make it EntityBase<T>
. A constraint is added to T
that is must be a type that inherits from EntityBase<T>
so that you can only pass domain entities as T
. This way, we now know what is the type of the entity at runtime for which the Equals
method is called.
Moreover, I have also overloaded the ==
and !=
operator. This comes in handy if you need to compare two instances of persistent entities in your code using these operators.
The modified code looks as follows:
namespace Domain { public class EntityBase<T> where T : EntityBase<T> { private int? hashCode; public override int GetHashCode() { if (hashCode.HasValue) return hashCode.Value; var transientEntity = Id == 0; if (transientEntity) { hashCode = base.GetHashCode(); return hashCode.Value; } return Id.GetHashCode(); } public virtual int Id { get; set; } public override bool Equals(object obj) { var other = obj as T; if (other == null) return false; var thisIsTransient = Id == 0; var otherIsTransient = other.Id == 0; if (thisIsTransient && otherIsTransient) return ReferenceEquals(this, other); return Id == other.Id; } public static bool operator ==(EntityBase<T> lhs, EntityBase<T> rhs) { return Equals(lhs, rhs); } public static bool operator !=(EntityBase<T> lhs, EntityBase<T> rhs) { return !Equals(lhs, rhs); } } }
Preceding code is all we need to make sure that two instances of entities in the memory pointing to same record in the database are treated as equal by NHibernate and CLR both. As we will see in Chapter 8, Using NHibernate in a Real-world Application, a generally recommended approach while developing web application using NHibernate is to use one session per incoming request. In such situation, most object equality needs are satisfied by NHibernate itself. But, it is still a good practice to implement the preceding code to ensure that you are not leaving some edge cases to chance.