开发者

Strongly typing ID values in C#

开发者 https://www.devze.com 2023-02-18 21:25 出处:网络
Is there a way to strongly type integer ID values in C#? I\'ve recently been playing with Haskell and can immediately see the advantages of its strong typing when applied to ID values, for example yo

Is there a way to strongly type integer ID values in C#?

I've recently been playing with Haskell and can immediately see the advantages of its strong typing when applied to ID values, for example you would never want to use a PersonId in place of a ProductId.

Is there a nice way to create an Id class/struct that can be used to represent IDs of a given开发者_开发问答 type?

I had the following idea but unfortunately it isn't legal on many levels. You can't have an abstract struct and the implicit/explicit cast operators wouldn't be inherited.

public abstract struct Id
{
    int _value;

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

   // define implicit Id to int conversion operator:
   public static implicit operator int(Id id) 
   {
      return _value;    
   }

   // define explicit int to Id conversion operator:
   public static explicit operator Id(int value) 
   {
      return new Id(value);
   }

   public bool Equals(object obj)
   {
      if(GetType() == obj.GetType()) 
      {
         Id other = (Id)obj;
         return other._value == _value;
      }
      return false;
   }

   public int GetHashCode()
   {
      return _value.GetHashCode();
   }
}

struct PersonId : Id { public PersonId(int value) : base(value) {} }
struct ProductId : Id { public ProductId(int value) : base(value) {} }

Are there any valid ways to perform something similar? How else can we prove that integer IDs type aren't being confused across a large application?


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 can be either structs or classes. You can even use the actual types that are identified by the id and in that case you do not need 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 allow safe and easy casting between id types and underlying id values. When working with legacy interfaces you may want to change the casting to integer to be implicit, or even better, to overload the legacy interfaces with properly typed versions. Extension methods can be used when the legacy interface is from a third party and cannot be changed 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 as suggested by smartcaveman.

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


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

SomeCompany.ID = SomePerson.ID

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

A less elegant solution involves defining your own value object type (as a class) and accessing the actual ID 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 write person.Id, you'll need to write person.Id.Value if you actually need the raw integer. It would be even better if you actually tried to reduce access to the raw integer value to as few places as possible, e.g. where you persist an entity to (or load it from) a DB.

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


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

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

Comparison operators, equality methods and GetHashCode are automatically implemented

To enforce value semantics, the compiler generates several methods for your record type (both for record class types and record struct types):

  • An override of Object.Equals(Object).
  • A virtual Equals method whose parameter is the record type.
  • An override of Object.GetHashCode().
  • Methods for operator == and operator !=.
  • Record types implement System.IEquatable.

Source


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

Basically the idea is to create an identity factory for each type, returning 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 a bit of work, and it won't work with an ORM, unless it has access to the internal struct, so unless that is a problem, the idea should be safe. This is a rough draft, and I haven't tested thoroughly myself, but so far I get value types for id's and there is no issue with the parameterless struct constructor.

If you like, the solution could be made generic as well (inspiration from the other posters), like 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 I'm not a huge fan of these kinds of generics, but I guess it's a matter of taste really. It's really just a way of constraining type compatibility, which in my opinion should be explicit. The advantage, though, is that you would only have a single place to do equality comparison, which is nice and DRY.

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


You create an abstract base class from which you can derive your strongly-typed identifier 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 abstract base class, you could also implement the IEquatable, IFormattable and the IComparable interfaces. You could also 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 provide static methods on the typed objects to conveniently create instances with empty 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 to allow implicit conversion, however that may arguably defeat the robustness and purpose of having the identifiers be strongly typed.


I'm not sure why you would want PersonID to be a class on its own, PersonID would be a property of person of type int.

Person.PersonID \\<<int

Now a long time ago I did see a nice pattern like this:

interface BusinessObject<TKey,TEntity> //TKey as in ID
{
 TKey ID;
 TEntity SearchById(TKey id);
}

So you could do this:

class Person : BusinessObject<Person, int> {}

OR

class FileRecord : BusinessObject<FileRecord, Guid> {}

Wonder if it helped but it just came to mind.

0

精彩评论

暂无评论...
验证码 换一张
取 消