The solution I propose involves quite a bit of code, but you can just copy it all and past it in a VS test solution assuming you have SqLite installed, and you should be able to
That's an interesting approach but instead of taking the time to understand and critique I will just offer my solution to this problem.
I don't like the idea of a generic entity base class, so my solution only supports int, Guid and string identities. Some of the code below, such as using a Func
to get the hash code, only exists to support case-insensitive string comparisons. If I ignored string identifiers (and I wish I could), the code would be more compact.
This code passes the unit tests I have for it and hasn't let me down in our applications but I'm sure there are edge cases. The only one I've thought of is: If I new and save an entity it will keep its original hash code, but if after the save I retrieve an instance of the same entity from the database in another session it will have a different hash code.
Feedback welcome.
Base class:
[Serializable]
public abstract class Entity
{
protected int? _cachedHashCode;
public abstract bool IsTransient { get; }
// Check equality by comparing transient state or id.
protected bool EntityEquals(Entity other, Func idEquals)
{
if (other == null)
{
return false;
}
if (IsTransient ^ other.IsTransient)
{
return false;
}
if (IsTransient && other.IsTransient)
{
return ReferenceEquals(this, other);
}
return idEquals.Invoke();
}
// Use cached hash code to ensure that hash code does not change when id is assigned.
protected int GetHashCode(Func idHashCode)
{
if (!_cachedHashCode.HasValue)
{
_cachedHashCode = IsTransient ? base.GetHashCode() : idHashCode.Invoke();
}
return _cachedHashCode.Value;
}
}
int identity:
[Serializable]
public abstract class EntityIdentifiedByInt : Entity
{
public abstract int Id { get; }
public override bool IsTransient
{
get { return Id == 0; }
}
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (EntityIdentifiedByInt)obj;
return Equals(other);
}
public virtual bool Equals(EntityIdentifiedByInt other)
{
return EntityEquals(other, () => Id == other.Id);
}
public override int GetHashCode()
{
return GetHashCode(() => Id);
}
}
Guid identity:
[Serializable]
public abstract class EntityIdentifiedByGuid : Entity
{
public abstract Guid Id { get; }
public override bool IsTransient
{
get { return Id == Guid.Empty; }
}
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (EntityIdentifiedByGuid)obj;
return Equals(other);
}
public virtual bool Equals(EntityIdentifiedByGuid other)
{
return EntityEquals(other, () => Id == other.Id);
}
public override int GetHashCode()
{
return GetHashCode(() => Id.GetHashCode());
}
}
string identity:
[Serializable]
public abstract class EntityIdentifiedByString : Entity
{
public abstract string Id { get; }
public override bool IsTransient
{
get { return Id == null; }
}
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (EntityIdentifiedByString)obj;
return Equals(other);
}
public virtual bool Equals(EntityIdentifiedByString other)
{
Func idEquals = () => string.Equals(Id, other.Id, StringComparison.OrdinalIgnoreCase);
return EntityEquals(other, idEquals);
}
public override int GetHashCode()
{
return GetHashCode(() => Id.ToUpperInvariant().GetHashCode());
}
}