6
\$\begingroup\$

Within our application I need to work a lot with simple IDs (just integers).

They are used to make queries against a database. For performance reasons I don't want to materialize them to Entities before the query.

Example:

// not really *that* static typed
public void DoQuery(int userId, int groupId)
{
}

Because I want to make our application more static typed (preventing errors like accidentally switching parameters around), I'm looking for a way to make this all more static typed, like:

public struct UserId
{
     int _UserId;

     public UserId(int userId)
     {
          _UserId = userId;
     }

    public static implicit operator int (UserId userId)
    {
        return userId._Id;
    }
}

and then:

// will throw compilation errors when I try to shoehorn a UserId into a GroupId
public void DoQuery(UserId userId, GroupId groupId)
{
}
  • Do I need to override equals operators and hascode, or would this be enough?
  • Are there any important performance penalties (during a highload)? For i.e. garbage collection.
  • What could be any more negative consequences from this?
\$\endgroup\$
2
  • 2
    \$\begingroup\$ +1 for good design & abstraction. It's easy to overlook creating types for this kind of thing and breaking away from Primitive Obsession. I like this very much, though many would say it's overkill. \$\endgroup\$
    – RubberDuck
    Commented Jan 23, 2016 at 2:05
  • \$\begingroup\$ Unit tests will catch all these problems for you before compiling thus removing any lingering problems, reducing code complexity and adding code coverage. The idea you're implementing is sometimes also referred to as 'Tiny Types' and while interesting is fairly tough to maintain. It's also an added complexity when you have to map this object to your ORM layer and back (to avoid creating 2 tables for one object), etc. TL;DR: use tests to make sure you're not switching two parameters. \$\endgroup\$ Commented Jan 23, 2016 at 4:31

2 Answers 2

4
\$\begingroup\$

You can try DRY here :) Define class like this (UPDATED to include ASP.NET support):

[TypeConverter(typeof(IdTypeConverter))]
[JsonConverter(typeof(IdJsonConverter))]
public struct Id<T> 
{
    public static explicit operator Id<T>(int value) => new Id<T>(value);
    public static implicit operator int(Id<T> id) => id.Value;

    public Id(int value)
       : this()
    {
        Value = value;
    }

    int Value { get; }

    public override int GetHashCode() => Value.GetHashCode();
    public override bool Equals(object obj) =>
          obj is Id<T> &&
          ((Id<T>)obj).Value == Value;

    public static bool operator ==(Id<T> left, Id<T> right) =>
          left.Value == right.Value;

    public static bool operator !=(Id<T> left, Id<T> right) =>
          left.Value != right.Value;
}

class IdJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return  
            objectType.IsConstructedGenericType && 
            objectType.GetGenericTypeDefinition() == typeof(Id<>);
    }

    public override object ReadJson(
         JsonReader reader, 
         Type objectType, 
         object existingValue, 
         JsonSerializer serializer)
    {
        return Activator.CreateInstance(
            objectType, 
            reader.ReadAsInt32());
    }

    public override void WriteJson(
          JsonWriter writer, 
          object value, 
          JsonSerializer serializer)
    {
        writer.WriteValue((int)(dynamic)value);
    }
}

class IdTypeConverter : TypeConverter
{
    public IdTypeConverter(Type type)
    {
        Type = type;
    }

    Type Type { get; }

    public override bool CanConvertFrom(
        ITypeDescriptorContext context, Type sourceType)
    {
        return
            sourceType == typeof(string) ||
            base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(
        ITypeDescriptorContext context, 
        CultureInfo culture, object value)
    {
        if (value is string)
            return Activator.CreateInstance(
                 Type, 
                 int.Parse(value as string));

        return base.ConvertFrom(context, culture, value);
    }

    public override bool CanConvertTo(
        ITypeDescriptorContext context, 
        Type destinationType)
    {
        return
            destinationType == typeof(string) ||
            base.CanConvertTo(context, destinationType);
    }

    public override object ConvertTo(
        ITypeDescriptorContext context, 
        CultureInfo culture, 
        object value, 
        Type destinationType)
    {
        if (destinationType == typeof(string))
            return $"{(int)(dynamic)value}";

        return base.ConvertTo(context, culture, value, destinationType);
    }
}

Let's imagine you have Entity Framework Entity Type User:

class User
{
   public int Id { get; set; } 
   public string Name { get; set; }
}  

Then:

IUserReader reader = new UserReader();
User dirkBoer = reader.Read(Users.DirkBoer);

Where:

class Users
{
   public static readonly Id<User> DirkBoer = (Id<User>)22;
   public static readonly Id<User> Jamal = (Id<User>)23;
}   

class UserReader : IUserReader
{
   public User Read(Id<User> id) => 
       Users().First(u => u.Id == id);

   IQueryable<User> Users() => 
       new EfContext()
         .Users
         .Include(u => u.Certificates);
} 
\$\endgroup\$
2
  • \$\begingroup\$ I would consider removing the first operator (implicit int to Id) as it somehow defeats the purpose (gives back the freedom to call DoQuery(1,2)). Anyway +1 \$\endgroup\$
    – user52292
    Commented Jan 24, 2016 at 15:52
  • \$\begingroup\$ @firda We might have it as explicit operator. It existance is a big deal for UserWriter where we need to assign User.Id (int) to DtoUser.Id (Id<User>). It also simplifies initialization and testing. I would say we need a way to be type safe, but we should not create artificial obstacles. P.S. I usually also inherit TypeConverter and JsonConverter to support ASP.NET serialization of DTO types where this Id<T> is used. \$\endgroup\$ Commented Jan 24, 2016 at 16:07
4
\$\begingroup\$

Do I need to override equals operators and hascode, or would this be enough?

Structs automatically implement Equals() and GetHashCode(), but that implementation is slow. If you're actually going to use them (even indirectly, e.g. by using some LINQ methods), and you care about performance, you should override them.

Are there any important performance penalties (during a highload)? For i.e. garbage collection.

There shouldn't be. GC doesn't get involved (unless you're going to box the values, but that's the same for integers).

I'm not sure if the runtime can optimize code using UserId as well as it could for an integer, so for performance critical code, you should make sure it's okay by profiling.

What could be any more negative consequences from this?

I think this is not a usual pattern in C#, so one negative consequence might be some confusion by your coworkers.

\$\endgroup\$
3
  • \$\begingroup\$ Hi @svick, thanks for your answer. When I need to implement Equals() GetHashCode and the == and != operators it starts to become quite a large piece of code (per Id). Do you see any way to get it more DRY? In a way I would prefer to just 'inherit' from int. \$\endgroup\$
    – Dirk Boer
    Commented Jan 23, 2016 at 2:23
  • 1
    \$\begingroup\$ @DirkBoer Not really in plain C#. You probably could use something like PostSharp to add that code after normal compilation. Or use ReSharper to automatically add the methods to your source; this won't make your code more DRY, but it will make it much easier to write (and avoid errors). \$\endgroup\$
    – svick
    Commented Jan 23, 2016 at 16:04
  • \$\begingroup\$ Thanks svick. I might see if it's possible to autogenerate these in the T4 template files within Entity Framework. \$\endgroup\$
    – Dirk Boer
    Commented Jan 23, 2016 at 16:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.