[ACCEPTED]-Strongly typing ID values in C#-c#

Accepted answer
Score: 31
public interface IId { }

public struct Id<T>: IId {
    private readonly int _value;

    public Id(int value) {
        this._value = value;
    }

    public static explicit operator int(Id<T> id) {
        return id._value;
    }

    public static explicit operator Id<T>(int value) {
        return new Id<T>(value);
    }
}

public struct Person { }  // Dummy type for person identifiers: Id<Person>
public struct Product { } // Dummy type for product identifiers: Id<Product>

Now you can use types Id<Person> and Id<Product>. The Person and Product types 17 can be either structs or classes. You can 16 even use the actual types that are identified 15 by the id and in that case you do not need 14 any dummy types.

public sealed class Person {
    private readonly Id<Person> _id;
    private readonly string _lastName;
    private readonly string _firstName;

    // rest of the implementation...
}

The explicit operator overloads 13 allow safe and easy casting between id types 12 and underlying id values. When working with 11 legacy interfaces you may want to change 10 the casting to integer to be implicit, or 9 even better, to overload the legacy interfaces 8 with properly typed versions. Extension 7 methods can be used when the legacy interface 6 is from a third party and cannot be changed 5 or overloaded directly.

public interface ILegacy {
    public bool Remove(int user);
}

public static class LegacyExtensions {
    public static bool Remove(this ILegacy @this, Id<Person> user) {
        return @this.Remove((int)user);
    }
}

Edit: Added IId interface 4 as suggested by smartcaveman.

Edit: Changed both 3 operators to be explicit after thinking 2 about Alejandro's suggestion and added a 1 section how to deal with legacy interfaces.

Score: 3

Well, in my experience, operator overloading 28 in such scenarios usually doesn't work too 27 well and can lead to all kinds of problems. Also, if 26 you provide implicit cast operators to/from 25 type int, are you sure that a statement such 24 as:

SomeCompany.ID = SomePerson.ID

will still get caught as invalid by the 23 compiler? Or might the compiler just use 22 your cast operators and thus let the invalid 21 assignment through...?

A less elegant solution 20 involves defining your own value object 19 type (as a class) and accessing the actual ID 18 via a Value property:

sealed class PersonId
{
    readonly int value;
    public int Value { get { return value; } }

    public PersonId(int value)
    {
        // (you might want to validate 'value' here.)
        this.value = value;
    }

    // override these in order to get value type semantics:
    public override bool Equals(object other) { … }
    public override int GetHashCode() { … }
}

Now, whenever you would 17 write person.Id, you'll need to write person.Id.Value if you actually 16 need the raw integer. It would be even better 15 if you actually tried to reduce access to 14 the raw integer value to as few places as 13 possible, e.g. where you persist an entity 12 to (or load it from) a DB.

P.S.: In the above 11 code, I would really like to make PersonId a struct, since 10 it's a value object. However, structs have one 9 problem, which is a parameter-less constructor 8 that's automatically provided by the compiler. This 7 means that a user of your type can bypass 6 all your constructors (where validation 5 might happen), and you might end up with 4 an invalid object right after construction. Thus, try 3 to make PersonId as similar to a struct as possible: by 2 declaring it sealed, by overriding Equals and GetHashCode, and 1 by not providing a parameterless constructor.

Score: 1

I know this is late for an answer (suggestion 30 actually), but I thought about this problem, and 29 toyed around with an idea, that might be 28 helpful.

Basically the idea is to create 27 an identity factory for each type, returning 26 the interface for the actual id instance.

public interface IPersonId
{
    int Value { get; }
}

public static class PersonId
{
    public static IPersonId Create(int id)
    {
        return new Id(id);
    }

    internal struct Id : IPersonId
    {
        public Id(int value)
        {
            this.Value = value;
        }

        public int Value { get; }

        /* Implement equality comparer here */
    }
}

It 25 a bit of work, and it won't work with an 24 ORM, unless it has access to the internal 23 struct, so unless that is a problem, the idea 22 should be safe. This is a rough draft, and 21 I haven't tested thoroughly myself, but 20 so far I get value types for id's and there 19 is no issue with the parameterless struct constructor.

If 18 you like, the solution could be made generic 17 as well (inspiration from the other posters), like 16 so:

public interface IId<T>
{
    int Value { get; }
}

public static class Id
{
    public static IId<T> Create<T>(int value)
    {
        return new IdImpl<T>(value);
    }

    internal struct IdImpl<T> : IId<T>
    {
        public IdImpl(int value)
        {
            this.Value = value;
        }

        public int Value { get; }

        /* Implement equality comparer here */
    } 
}

Then you would be able to do Id.Create<Person>(42).

Personally 15 I'm not a huge fan of these kinds of generics, but 14 I guess it's a matter of taste really. It's 13 really just a way of constraining type compatibility, which 12 in my opinion should be explicit. The advantage, though, is 11 that you would only have a single place 10 to do equality comparison, which is nice 9 and DRY.

It's definitely not perfect and 8 in most (if not the majority) of use cases 7 it would be completely over engineered, just 6 to get around the fact that structs has a default 5 parameterless constructor. Furthermore there 4 is no way to do validation checks on the 3 generic variation, for obvious reasons, and 2 the underlying id value is constrained to 1 int (which could be changed of course).

Score: 1

You create an abstract base class from which 11 you can derive your strongly-typed identifier 10 objects.

public abstract class TypedGuid
{
    protected TypedGuid(Guid value)
    {
        Value = value;
    }

    public Guid Value { get; }
}

Then you inherit from that:

public class ProductId : TypedGuid
{
    public ProductId (Guid value) : base(value)
    { }
}

In the 9 abstract base class, you could also implement 8 the IEquatable, IFormattable and the IComparable interfaces. You could also 7 override the GetHashCode and ToString methods.

public abstract class TypedGuid
{
    protected TypedGuid(Guid value)
    {
        Value = value;
    }

    public bool HasValue => !Value.Equals(Guid.Empty);
    public Guid Value { get; }

    public override int GetHashCode() => Value.GetHashCode();
    public override string ToString() => Value.ToString();
}

You could also 6 provide static methods on the typed objects 5 to conveniently create instances with empty 4 values.

public class ProductId : TypedGuid
{
    public static ProductId Empty => new ProductId(Guid.Empty);
    public static ProductId New => new ProductId(Guid.NewGuid());

    public ProductId(Guid value) : base(value)
    {
    }
}

You could also add implicit operators 3 to allow implicit conversion, however that 2 may arguably defeat the robustness and purpose 1 of having the identifiers be strongly typed.

Score: 0

From C#9 forward you can use records to define 1 strongly typed IDs.

public record ModelId(Guid Id);
public record MyModel(ModelId Id, string Data /*...*/);

Documentation about records

More Related questions