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") { } }