It's not that uncommon to use an ID for a data object.  However, sometimes more information is needed to uniquely identify an object than a number.  In these cases is useful to use another object.  It can contain an integer, guid, date created, created by, or even the date and time and object was modified.

A good example of this would be a content management system.  A given piece of content could have a content object ID, which would contain an ID, the day and time of the modification, who modified the content and a revision number.  This would allow content to be retrieved not only by an ID but by revision allowing the content management system to save a history of the edits made to the content as well as draft's of pending revisions.

Another example would be in a customer records management system.   An address, an e-mail, a phone number, or instant messenger ID are all types of a contact information.  An abstract contact information base class could use an integer as an identifier for the object; however, if we used a contact information identifier object we would be able to inherit the identifier object to determine the type of relationship or parent object with contact information belong to.  For example, a phone number can belong to a person or a company.  We could use a person contact information identifier object or a company contact information identifier object inherited from an abstract contact information identifier object to determine what the type of parent object is in code. 

The abstract contact information identifier object (that's a mouthful isn't it? ) could contain an int32 or int64 as an ID field.  It would also have a parent ID field. 

The hard part of using an object for an id is giving up the easy "Id=5;" syntax.  But who says that needs to be given up?

We all have overloaded a method or two at some point.  But what about overloading an operator? 

public class ContactInformationId
    {
        public ContactInformationId(Int32 id)
        {
            Id = id;
        }
        public Int32 Id { get; set; }

        public static implicit operator ContactInformationId(Int32 id)
        {
            return new ContactInformationId(id);
        }

        public static implicit operator Int32(ContactInformationId id)
        {
            return id.Id;
        }
    }

Ah ha! So if we took the following code not only would it compile but at the end id.Id would equal 12.

ContactInformationId id = 5;
id = 10;
if (id < 7) id = 13;
if (id > 7) id = 2;
if (id == 5) id = 6;
if (6 != id) id = 12;

But it is not all roses.  If you add another property...

    public class ContactInformationId
    {
        public ContactInformationId(Int32 id)
        {
            Id = id;
        }
        public Int32 Id { get; set; }
        public Int32 version { get; set; }

        public static implicit operator ContactInformationId(Int32 id)
        {
            return new ContactInformationId(id);
        }

        public static implicit operator Int32(ContactInformationId id)
        {
            return id.Id;
        }
    }

and you run the following code

            ContactInformationId id = 5;
            id.version = 2;
            id = 10;

            if (id < 7) id = 13;
            if (id > 7) id = 2;
            if (id == 5) id = 6;
            if (6 != id) id = 12;

id.Id will still be 12; but id.Version will be 0.

Every time the ContactInformationId object uses the operator overload it return a new object "crushing" any other properties.  In the case of an id this might not be so bad.  In a Content Management System (CMS) a version number of 0 could be used to call the "Head" version.  But in cases where, "I don't need that, I only need an Int32," there is a greater "hidden" value.

Lets say I have a Person object with a PersonId object in it for an Id property.  I use lazy loading to get back a list of contact information.  I call ContactInformationDataService.GetByParentId(somePerson.Id);

Now lets say I have a Company object with a CompanyId in it for an Id property. Again I call ContactInformationDataService.GetByParentId(someCompany.Id);

if the Id properties were just Int32 there would be no way to know what return; after all company id 5 and person id 5 look the same as an Int32.  But when they are a PersonId and CompanyId object we can use standard method overloading to know what we are returning.

Take this another step and inherit the id objects from a common base class like...BaseId.  Now you can have a method in your data layer like GetByParentId(BaseId id) and, in the case of SQL server, set the stored procedure name based on the type of id object. 

if(id is PersonId) sprocName="usp_ContactInformation_GetByPersonId";
else if(id is CompanyId) sprocName="usp_ContactInformation_GetByCompanyId";

If you are a big xml fan, you can set the file name or top node based on the id type.

More importantly you can use overloads on the methods to take action based on the type*.

protected override ContactInformationList GetContactInformationByParentId(SomeBaseId id)
{
     if(id is PersonId) GetContactInformationByParentId((PeronsId)id);
     if(id is CompanyId) GetContactInformationByParentId((CompanyId)id);
}

private ContactInformationList GetContactInformationByParentId(PersonId id)
{
     ...
}

private ContactInformationList GetContactInformationByParentId(CompanyId id)
{
     ...
}

*Note: the Microsoft .Net Framework maps methods at compile time not run time.  So if a variable is boxed into a base type it will go to the method that uses the "boxed" type in the signature, not the type of object stored in the box at run time.  Always pay attention to how your variables are boxed when you pass them.  This is why logic like if(id is PersonId) in the GetContactInformationByParentId(BaseId id) method is needed.

I normally just declare my overloads in the abstract provider and avoid the type checking.

protected abstract ContactInformationList GetContactInformationByParentId(PersonId id);
protected abstract ContactInformationList GetContactInformationByParentId(CompanyId id);

In the end, it makes for somewhat cleaner code (it would be even cleaner if .net supported runtime type checking).  While the Id object does have an issue with crushing itself when assigned to...that is the expected behavior of an object that it is overwritten when we assign to it instead of a property.

Below is an example of an abstract base class and subclass.  Something like GetByParentId(GenericId<Int32> id) would be extremely useful.  Notice the operator overloads are on the ContactInformation and not the abstract class.  This has to do with operator overloads needing to be "contained" within the type they are converting to or from.  In other words, they have to be there to prevent errors at compile time.

    public abstract class GenericId<BaseIdType> 
    {
        public GenericId(BaseIdType id)
        {
            Id = id;
        }
        public BaseIdType Id { get; set; }
    }

    public class ContactInformationId : GenericId<Int32>
    {
        public ContactInformationId(Int32 id) : base(id)
        {
            
        }
        public static implicit operator ContactInformationId(Int32 id)
        {
            return new ContactInformationId(id);
        }

        public static implicit operator Int32(ContactInformationId id)
        {
            return id.Id;
        }
    }

This solves a few issues for me when I am going back and forth between an object model and a relational data source like SQL Server.

While the operator overloads make things a bit easier, they are not really required.

You could get fancy and create an IIdentifier interface that defines an Id property

public interface IIdentifier
{
    object Id { get; set; }
}

Now if you explicitly implement the IIdentifier interface or your base GenericId<BaseIdType> class like...

        object IIdentifier.Id
        {
            get
            {
                return this.Id;
            }
            set
            {
                if (value is BaseIdType)
                    this.Id = (BaseIdType)value;
                else
                    throw new InvalidCastException(value.GetType().ToString() + " can not be cast as a " + typeof(BaseIdType).ToString());
            }
        }

You can use the IIdentifier in your method signatures and not worry about the type of the internal Id....That means PersonId could be a Guid and CompanyId remains an Int32 and nothing explodes. 

There is one little trick, you will want to implement the IEquatable<IIdentifier> on the GenericId<BaseIdType> class.

        public bool Equals(IIdentifier other)
        {
            bool result = false;
            if (this.GetType()==other.GetType())            {
                result = Id.Equals(other.Id);
            }
            return result;
        }

We start off saying they are not equal.  We check the types to see if they are the same type and we call the equals method of the Id property and pass in the other identifiers Id property.  Because other is boxed as an IIdentifier this.Id==other.Id will always be false. 

Well not "always", the Id property is boxed as an object and the default Equals method on an object checks to see if the two objects are both references to the same instance of an object (and while it is possible that they might be; they very rarely will be because of the nature of Int32, Guid, decimals and structs in general). See my blog about variable boxing and == vs. Equals for more information.

Calling the equals method instead of using the == operator invokes the correct Equals method on the GenericId<BaseIdType> instead of the default object equals method...it has to do with compile time mapping of methods.

If you add the ICloneable interface to the object as well you have a very nice little object that you can clone, pass around and do anything you need to do with it.

I have seen people put methods in their identity object to get or delete an object with that id or return a list of child/parent objects. 

What does all of this do?  It gives us a non generic type to use in our method signatures, a strongly typed generic base class and strongly typed sub classes to use in our methods.  

Finally, it does one more thing.  When someone changes the requirement from the Id being an Int32 to a Guid the the logic of the Id is separated from your code making it a little easier to change.