Entity Classes, IDs and Equality


I want my entity classes to rely on their ID when checking equality, the ID is populated from the repository when I retrieve an object and automatically generated when I save a new entity.

This poses the problem of how I compare two entities before they have been saved while working with them in my domain, or, when I have ‘newed’ them up myself for unit testing. The answer is to use a transient ID.

Normally an Entity Base class might look like this:

public abstract class EntityBase : IEquatable<EntityBase> {

    public virtual int Id { get; protected set; }

    public override bool Equals(object other) {
        return Equals(other as EntityBase);
    }

    public override int GetHashCode() {
        return Id;
    }

    public virtual bool Equals(EntityBase other) {
        if (other == null) return false;
        return other.Id == Id && other.GetType() == GetType();
    }

}

If two new objects are created from a derived class then object1.Equals(object2) will be true as Id will be 0 for both objects. To avoid this I need to check if an object has not yet been saved:

private bool IsTransient {
    get { return Id == 0; }
}

And just check for referential equality if this is the case:

public virtual bool Equals(EntityBase other) {
    if (other == null) return false;
    if (IsTransient) return ReferenceEquals(this, other);
    return other.Id == Id && other.GetType() == GetType();
}

GetHashCode() needs to be overridden to reflect this:

public override int GetHashCode() {
    if (IsTransient) return base.GetHashCode();
    return Id;
}

The full code for EntityBase is:

public abstract class EntityBase : IEquatable<EntityBase> {

    public virtual int Id { get; protected set; }

    private bool IsTransient {
        get { return Id == 0; }
    }

    public override bool Equals(object other) {
        return Equals(other as EntityBase);
    }

    public override int GetHashCode() {
        if (IsTransient) return base.GetHashCode();
        return Id;
    }

    public virtual bool Equals(EntityBase other) {
        if (other == null) return false;
        if (IsTransient) return ReferenceEquals(this, other);
        return other.Id == Id && other.GetType() == GetType();
    }

}

, , , , , , , , ,

  1. #1 by xhafan on April 2, 2012 - 10:14

    GetHashCode() value must not change for the lifetime of the object if you add the object into a dictionary or hashtable. http://stackoverflow.com/questions/462451/gethashcode-guidelines-in-c-sharp http://stackoverflow.com/questions/7687551/ddd-gethashcode-and-primary-id

  2. #2 by John on April 2, 2012 - 10:57

    Thanks Xhafan,
    Those are good points and there are other solutions such as caching the hashcode for the lifetime of the object. But there is a trade off, whichever solution you take. As mentioned in the links, not changing the hashcode is a guideline rather than a rule, but it is definitely worth pointing out that a persisted object != an unpersisted one.

    The actual rule is:
    Rule: the integer returned by GetHashCode must never change while the object is contained in a data structure that depends on the hash code remaining stable.

    Thanks for pointing that out.

(will not be published)