Generic Entity Equality
Just got bit by reference equality vs. value equality again. Since this is the third time that I am writing this base class, I decided to put it in the blog for future reference (I wrote it first and forgot to overload the == / != operators).
By the way, this is the perfect example of a Mixin.
Updated: Ben Scheirman reminded me that I need to handle trasient objects as well.
/// <summary>
/// This is a trivial class that is used to make sure that Equals and GetHashCode
/// are properly overloaded with the correct semantics. This is exteremely important
/// if you are going to deal with objects outside the current Unit of Work.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TKey"></typeparam>
public abstract class EqualityAndHashCodeProvider<T, TKey>
where T : EqualityAndHashCodeProvider<T, TKey>
{
private int? oldHashCode;
/// <summary>
/// Determines whether the specified <see cref="T:System.Object"></see> is equal to the current <see cref="T:System.Object"></see>.
/// </summary>
/// <param name="obj">The <see cref="T:System.Object"></see> to compare with the current <see cref="T:System.Object"></see>.</param>
/// <returns>
/// true if the specified <see cref="T:System.Object"></see> is equal to the current <see cref="T:System.Object"></see>; otherwise, false.
/// </returns>
public override bool Equals(object obj)
{
T other = obj as T;
if (other == null)
return false;
//to handle the case of comparing two new objects
bool otherIsTransient = Equals(other.Id ,default(TKey));
bool thisIsTransient = Equals(this.Id, default(TKey));
if (otherIsTransient && thisIsTransient)
return ReferenceEquals(other, this);
return other.Id.Equals(Id);
}
/// <summary>
/// Serves as a hash function for a particular type. <see cref="M:System.Object.GetHashCode"></see> is suitable for use in hashing algorithms and data structures like a hash table.
/// </summary>
/// <returns>
/// A hash code for the current <see cref="T:System.Object"></see>.
/// </returns>
public override int GetHashCode()
{
//This is done se we won't change the hash code
if (oldHashCode.HasValue)
return oldHashCode.Value;
bool thisIsTransient = Equals(this.Id, default(TKey));
//When we are transient, we use the base GetHashCode()
//and remember it, so an instance can't change its hash code.
if (thisIsTransient)
{
oldHashCode = base.GetHashCode();
return oldHashCode.Value;
}
return Id.GetHashCode();
}
/// <summary>
/// Get or set the Id of this entity
/// </summary>
public abstract TKey Id { get; set; }
/// <summary>
/// Equality operator so we can have == semantics
/// </summary>
public static bool operator ==(EqualityAndHashCodeProvider<T, TKey> x, EqualityAndHashCodeProvider<T, TKey> y)
{
return Equals(x, y);
}
/// <summary>
/// Inequality operator so we can have != semantics
/// </summary>
public static bool operator !=(EqualityAndHashCodeProvider<T, TKey> x, EqualityAndHashCodeProvider<T, TKey> y)
{
return !(x == y);
}
}
Comments
Does the where clause of the generic definition specify infinite recursion, or am I being retarded? Is it possible to specify types to it that satisfy the where condition?
This class will satisfy the constraint:
class B : EqualityAndHashCodeProvider<B, TKey>
For the equals implementation, the base class must know who derived from it, so one can use the type parameter T to tell it.
I think I have also done this somewhere where I needed a mixin.
Ahh, I was thinking that the recursion would be satisfiable by inheriting the class, but I got lazy. :-( I had just finished solving a really annoying bug and felt like taking a short break from thinking rigorously; oops! I'm going to have to remember this, ahem, design pattern.
Oren, isn't there an issue with using the PK as as HashCode source?
Take for example you are creating 2 objects, both transient, but they have different properties. Since they haven't been persisted yet, they have the same key value (0, Guid.Empty, null, whatever) and thus will get the same hash code.
This will place those 2 objects in the same slot in the Cache, which will lead to strange issues.
This is mentioned in Hibernate In Action, so I wanted to hear your input on this...
Ben, you are correct, a special case can be made for:
if ( other.Id == default(TKey) && this.Id == default(Tkey) )
return ReferenceEquals(other, this);
Updated the post
Can this go into ActiveRecord?
@Jesus,
Not really. AR needs to support more stuff, like multi key entities
This got me all inspired to create a base type for DDD Value Objects.
http://grabbagoft.blogspot.com/2007/06/generic-value-object-equality.html
Comment preview