Over the years I have tried a number of Identifier schemes:

  • Atomic: String UserId; int UserId; Guid UserId;
    • There is no link to the underlying domain type.
    • There instances are easily confused. I.e. A page taking a Url param of "Id=123" may refer to a User, UserGroup, UserProfile, NTLM, Email, etc. This gets significantly worse in complex enterprise systems, eg. "ServiceLocationIdentifier" may be named as a local variable called "sl" and the developer needs to correctly interpret this.
  • Classic Strongly Typed: class UserIdentifier { string UserID { get; set; } }
    • This is significantly better, but there is the overhead of creating and maintaining all these types
    • There is no scheme in place to unify them. It is possible, ofcourse, to create an class abstract BaseIdenfifier { ... }
    • There is no coupling from the Identifier to the Domain Type, ie User and UserIdentifer as not linked by the scheme. Meaning you would have to build this in with every class

The code of the idea I am playing with is:

public class Id<T>
{
    public string Id { get; set;}
    public Type DomainType { get; set; }
    public override string ToString() { return Id; }
    public override int GetHasCode() { return Id.GetHashCode(); }
}

This code as the following properties (none very interesting):

  • The DomainType (T) is strongly maintained.
    • This means that new Id<User>("Bob") == new Id<Group>("Bob") , should throw an TypeMismatch exception
  • You can treat it as a string new Id<User>("Bob") == "Bob") will evaluate as true.
  • There is a string coupling between the Identifier and the Domain Type
    • class User { public Id<User> UserId { get; set; } }

The fun stuff, however, happens when you introduce a 'Services' class:

public class static Services
{
    public T GetInstance(Id<T> Identifier) { ... }
    public Uri GetUri(Id<T> Identifier) { ... }
    public string GetDisplayName(Id<T> Identifier) { ... }
}

The result of which are statements like:

public void ExampleCode()
{
    Id<User> bob = new Id<User>("Bob");
    string EmailText = string.Format("Hello {0}, Please click on the following link to edit your details {1}.",
           Services.GetDisplayName(bob),
           Services.GetUri(bob));

    User bobDetails = Services.GetInstance(bob);
    SmtpHelper.SendEmail(bobDetails.Email, EmailText, "Welcome...");
}

The full version of the class is as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;

namespace GL.General.Core
{
    /// <summary>
    /// Encapsulated the relationship between a type and its string identifier.
    /// It provides a type-strong relationship, in which for example a UserId and SaleId are NOT comparible
    /// </summary>
    /// <typeparam name="T">The domain type associated with the identifier</typeparam>
    public class Id<T>
    {
        /// <summary>
        /// Default Constructor
        /// </summary>
        public Id()
        {
            DomainType = typeof(T);
            ID = null;
        }

        /// <summary>
        /// Strong constructor
        /// </summary>
        /// <param name="id"></param>
        public Id(string id)
        {
            DomainType = typeof(T);
            ID = id;
        }

        /// <summary>
        /// Copy constructor
        /// </summary>
        /// <param name="toCopy"></param>
        public Id(Id<T> toCopy)
        {
            DomainType = typeof(T);
            ID = toCopy.ID;
        }

        /// <summary>
        /// The string Identifier. The value of the identfier
        /// </summary>
        [XmlAttribute]
        public string ID { get; set; }

        /// <summary>
        /// The type of the identifier (used for meta-data and validation only)
        /// </summary>
        [XmlIgnore]
        public Type DomainType { get; set; }

        /// <summary>
        /// This is a helper method, which provides the <see cref="DomainType"/> which is serialised in XML
        /// </summary>
        /// <remarks>
        /// This attribute, while serialised to XML, is ignored when it is read in
        /// </remarks>
        [XmlAttribute]
        public string Class
        {
            get
            {
                if (DomainType == null) return string.Empty;
                return DomainType.FullName;
            }
            set
            {
                // Ignored in deserialisation
            }
        }

        /// <summary>
        /// Provide Equality checking with Id<T> and string
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            if (obj == null && ID == null) return true;
            if (obj != null && ID != null) return obj.ToString().Equals(ID);
            return false;
        }

        /// <summary>
        /// Provide Equality checking with Id<T> and string
        /// </summary>
        public static bool operator ==(Id<T> lhs, Id<T> rhs)
        {
            if ((object)lhs == null && (object)rhs == null) return true;
            if ((object)lhs != null && (object)rhs != null) return lhs.ID == rhs.ID;
            return false;
        }

        /// <summary>
        /// Provide Equality checking with Id<T> and string
        /// </summary>
        public static bool operator !=(Id<T> lhs, Id<T> rhs)
        {
            return !(lhs == rhs);
        }

        /// <summary>
        /// Provide Equality checking with Id<T> and string
        /// </summary>
        public static bool operator ==(Id<T> lhs, string rhs)
        {
            if ((object)lhs == null && (object)rhs == null) return true;
            if ((object)lhs != null && (object)rhs != null) return lhs.ID == rhs;
            return false;
        }

        /// <summary>
        /// Provide Equality checking with Id<T> and string
        /// </summary>
        public static bool operator !=(Id<T> lhs, string rhs)
        {
            return !(lhs == rhs);
        }

        /// <summary>
        /// Provide Equality checking with Id<T> and string
        /// </summary>
        public static bool operator ==(string lhs, Id<T> rhs)
        {
            if ((object)lhs == null && (object)rhs == null) return true;
            if ((object)lhs != null && (object)rhs != null) return lhs == rhs.ID;
            return false;
        }

        /// <summary>
        /// Provide Equality checking with Id<T> and string
        /// </summary>
        public static bool operator !=(string lhs, Id<T> rhs)
        {
            return !(lhs == rhs);
        }

        /// <summary>
        /// ToString will provide the underling string identifier
        /// </summary>
        public override string ToString()
        {
            return ID;
        }

        /// <summary>
        /// Use the underlying string as a HashCode
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            if (ID == null) return 0;
            return ID.GetHashCode();
        }
    }
}