In this recipe, I'll show you how to set up a base class to use for your entities.
Entity.cs
, use the following code for the Entity
class:public abstract class Entity<TId> { public virtual TId Id { get; protected set; } public override bool Equals(object obj) { return Equals(obj as Entity<TId>); } private static bool IsTransient(Entity<TId> obj) { return obj != null && Equals(obj.Id, default(TId)); } private Type GetUnproxiedType() { return GetType(); } public virtual bool Equals(Entity<TId> other) { if (other == null) return false; if (ReferenceEquals(this, other)) return true; if (!IsTransient(this) && !IsTransient(other) && Equals(Id, other.Id)) { var otherType = other.GetUnproxiedType(); var thisType = GetUnproxiedType(); return thisType.IsAssignableFrom(otherType) || otherType.IsAssignableFrom(thisType); } return false; } public override int GetHashCode() { if (Equals(Id, default(TId))) return base.GetHashCode(); return Id.GetHashCode(); } }
Entity
class as shown in the following code:public abstract class Entity : Entity<Guid> { }
NHibernate relies on the
Equals
method to determine equality. The default behavior defined in System.Object
uses reference equality for reference types, including classes. That is, x.Equals(y)
is only true when x
and y
point to the same object instance. This default works well in most cases.
To support lazy loading, NHibernate uses proxy objects. As we learned in the previous recipe, these proxy objects are subclasses of the real entity class, with every member overridden to enable lazy loading.
This combination of proxy objects and the default Equals
behavior can lead to subtle and unexpected bugs in your application. An application should not be aware of proxy objects, and therefore would expect that a proxy and a real instance representing the same entity would be equal. A Product
instance with an ID of 8 should be equal to a different Product
instance or Product
proxy with an ID of 8. To handle this, we must override the default Equals
behavior.
On our Entity
base class, we override the Equals
method to determine equality based on POID. In Equals(Object obj)
, we simply call Equals(Entity<TId> other)
, attempting to cast the object to Entity
. If it can't be cast, null
is passed instead.
If other
is null
, the objects are not equal. This serves two purposes. First, x.Equals(null)
should always return false
. Second, someEntity.Equals(notAnEntity)
should also return false
. Next, we compare references. Obviously, if two variables reference the same instance, they are equal. If ReferenceEquals(this, other)
returns true
, we return true
.
Next, we compare the Ids
to the default value to determine if the entities are transient. A
transient object is an object that has not been persisted to the database. default(TId)
returns whatever the default may be for TId
. For Guids
, the default is Guid.Empty
. For strings and all other reference types, it's null
. For numeric types, it's zero. If the Id
property equals the default value, the entity is transient. If one or both entities are transient, we give up and return false
.
If both entities are persisted, they both have POIDs. We can compare these POIDs to determine equality. If the POIDs don't match, we know for certain that the two entities are not equal. We return false
.
Finally, we have one last check. We know that both entities are persistent, and they have the same Id
. This doesn't quite prove that they're equal. It's perfectly legal for an ActorRole
entity to have the same POID as a Product
entity. Our last check is to compare the types. If one type is assignable to the other type, then we know for certain that the two are equal.
Suppose other
is a proxy of Product
representing a book entity, and this
is an actual Book
instance representing the same entity. this.Equals(other)
should return true
because they both represent the same entity. Unfortunately, other.GetType()
will return the type ProductProxy12398712938
instead of the type Product
. As typeof(ProductProxy12398712938).IsAssignableFrom(typeof(Book))
returns false
, our Equals
would fail on this case. However, we can use other.GetUnproxiedType()
to reach down through the proxy layer and return the entity type. Because typeof(Product).IsAssignableFrom(typeof(Book))
returns true
, our Equals
implementation works.
Because we've overridden Equals
, we also need to override GetHashCode
to satisfy the requirements of the .NET Framework. Specifically, if x.Equals(y)
, then x.GetHashCode()
and y.GetHashCode()
should return the same value. The inverse is not necessarily true, however; x
and y
may share a hash code even when they're not equal. In our Entity
base class, we simply use the hash code of Id
, as this is the basis of our equality check.
For more information on Equals
and GetHashCode
, refer to the MSDN documentation for these methods at http://msdn.microsoft.com/en-us/library/system.object.aspx.