I like to use abstract base classes for my value objects. One of my most used is for identities:
public abstract class Identity<TId>
where TId : Identity<TId>
{
private readonly string _prefix;
public abstract string Value { get; protected set; }
/* ... */
protected Identity(string value, string prefix)
: this(prefix)
{
Value = value;
}
// So we can deserialize and have access to the prefix
protected Identity(string prefix)
{
_prefix = prefix;
}
}
Since the identities are value objects, we need to make sure that any two identities of the same type with the same values are considered equal.
public abstract class Identity<TId> : IEquatable<TId>
where TId : Identity<TId>
{
private readonly string _prefix;
public abstract string Value { get; protected set; }
public bool Equals(TId other)
{
if (ReferenceEquals(null, other)) return false;
return ReferenceEquals(this, other) || Equals(Value, other.Value);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, other)) return false;
return ReferenceEquals(this, obj) || Equals(obj as TId);
}
public override int GetHashCode()
{
unchecked
{
var hash = 17;
hash += hash * 23 + Value.GetHashCode();
hash += hash * 23 + GetType().GetHashCode();
return hash;
}
}
public override string ToString() { return string.Format("{0}/{1}", _prefix, Value); }
protected Identity(string value, string prefix)
: this(prefix)
{
Value = value;
}
// So we can deserialize and have access to the prefix
protected Identity(string prefix)
{
_prefix = prefix;
}
}
Almost...
This works for most situations but, recently, I found out that there's a little bit of a bug with the implementation of GetHashCode(). The overall algorithm is pretty sound for most cases, minus one little piece: GetType().GetHashCode(). The way I found out this doesn't work is by having to persist the hash between uses of an app. Bringing the app down and back up caused GetType().GetHashCode() to return a different value. Whoops!
(The reason for GetType().GetHashCode() was so that two identities of different types with the same .Value; would return different hashes.)
I switched it up, for now, to use GetType().Name. Fixed the issue for me.
public abstract class Identity<TId> : IEquatable<TId>
where TId : Identity<TId>
{
/* ... */
public override int GetHashCode()
{
unchecked
{
var hash = 17;
hash += hash * 23 + Value.GetHashCode();
hash += hash * 23 + GetType().Name.GetHashCode();
return hash;
}
}
/* ... */
}
And, to wrap up the use of this class, here's a sample of how to use it (fully serializable, too):
[DataContract(Namespace = "my-namespace")]
public abstract class ProductId : Identity<ProductId>
{
[DataMember(Order = 1)]
public override string Value { get; protected set; }
public ProductId(long id)
: base(id.ToString(CultureInfo.InvariantCulture), "product") { }
private ProductId()
: base("product") { }
}