Typed Identifiers

Strongly Typed Identifiers in .NET revealed

Intro

Have you ever seen the code like this one below?

public void AssignDuty(int customerId, int dutyId);

I am sure you did! Everyone did! And how about that?

public double CalculateRate(int customerId, DateTime from, DateTime to);

What’s wrong with it? Try to think and in the meantime I will show you the real life case I bumped into.

Here is the interface:

interface IScheduler
{
    void AssignDuty(int customerId, int dutyId);
}

and here is its implementation:

class Scheduler : IScheduler
{
    public void AssignDuty(int dutyId, int customerId)
    {
        //some logic here
    }
}

Did you spot the problem? The parameters in the implementation are swapped…

Problem

Quite often the methods have multiple parameters of the same type, like multiple integers or DateTimes. Sometimes it is just a nature of data, like identifiers are usually integers (AssignDuty). In other cases this is a missing abstraction for an element being processed, like in the CalculateRate function.
Despite the nature of the case, it is problematic place in the code which can make you mess up the parameters (like passing customerId instead of dutyId). The old Chinese proverb says:

If your function has 2 parameters of the same type, it is something wrong with your function.

If your function has 3 or more parameters of the same type, it is something wrong with you.

CalculateRate function can be easily fixed by introducing the abstraction like TimeInterval, so the code will look like:

public double CalculateRate(int customerId, TimeInterval interval);

What to do in the case of AssignDuty? The answer is…

Strongly Typed Identifiers

This is just a class or struct which wraps your identifier. The naive approach may look like this:

public class CustomerId
{
    public int Id { get; set; }
}

So now, AssignDuty function has signature like:

public void AssignDuty(CustomerId customerId, DutyId dutyId);

And from that point the function is not prone to parameters swap. There is also an impossibility to make a mistake like with IScheduler interface.

Can we do it better?

Yes, we can! Let’s adjust our Strongly Typed Identifiers to be more cool.

No default value

You should know the value of identifier when you create CustomerId. Scenario like “create CustomerId and fill value later” is unacceptable. So let’s get rid of the default, parameterless constructor and add one with value to initialize the instance.

public class CustomerId
{
    public int Id { get; set; }

    public CustomerId(int id)
    {
        Id = id;
    }
}

Immutability

It would be nice to have a value which is immutable (you can not change it). When you assign id, it is like it was written on a paper, like a photocopy. You should not be afraid that someone else will change that value in the meantime, e.g. when AssignDuty returns, CustomerId’s value should be preserved. Let’s make Id field readonly!

public class CustomerId
{
    public int Id { get; }

    public CustomerId(int id)
    {
        Id = id;
    }
}

Explicit Unknown value

In many scenarios you need to process entity with no identifier assigned, e.g. identifier will be assign by database’s auto-increment column. Let’s have an explicit object for the unknown value.

public class CustomerId
{
    public int Id { get; }

    public CustomerId(int id)
    {
        Id = id;
    }

    public static readonly CustomerId Unknown = new CustomerId(0);
}

Make me sealed

This is just an identifier, nothing else. It is hard to imagine the scenario where you need to inherit from CustomerId. Let’s seal the class and do not bother about derived types when comparing our Strongly Typed Identifiers. It will also disallow unintended usage, i.e. deriving instead of composition.

public sealed class CustomerId
{
    public int Id { get; }

    public CustomerId(int id)
    {
        Id = id;
    }

    public static readonly CustomerId Unknown = new CustomerId(0);
}

Compare me

Three things in the world we take for granted: taxes, death and comparing identifiers. You need to determine whether objects are the same or not. That’s why your entities have identity. Let’s implement IEquatable in a proper way!

    public sealed class CustomerId : IEquatable<CustomerId>
    {
        public int Id { get; }

        public CustomerId(int id)
        {
            Id = id;
        }

        public static readonly CustomerId Unknown = new CustomerId(0);

        public override bool Equals(object obj)
            => this.Equals(obj as CustomerId);

        public bool Equals(CustomerId p)
        {
            if (Object.ReferenceEquals(p, null))
            {
                return false;
            }

            if (Object.ReferenceEquals(this, p))
            {
                return true;
            }

            return Id == p.Id;
        }

        public override int GetHashCode()
            => this.Id;

        public static bool operator ==(CustomerId lhs, CustomerId rhs)
        {
            if (Object.ReferenceEquals(lhs, null))
            {
                if (Object.ReferenceEquals(rhs, null))
                {
                    return true;
                }

                return false;
            }

            return lhs.Equals(rhs);
        }

        public static bool operator !=(CustomerId lhs, CustomerId rhs)
            => !(lhs == rhs);
    }

Class vs Struct

As I mentioned, you can model Strongly Typed Identifiers as a class or as a struct. Let’s point out the advantages and disadvantages of both approaches.

Class

Class has only one, but crucial feature: you can get rid of parameterless constructor, so you can take full control of creation process.

Struct

You cannot remove parameterless constructor, so at any point in time, the user can create object with any value (he can even duplicate the special, unique Unknown instance). The only way to mitigate this problem is to prepare convention test which checks all usages of parameterless constructor of the type and fails if any exists.

The struct gives you immutability by design, which is nice. When your identifier contains only a few fields, structs are faster to process. Also remember about proper implementation of IEquatable interface.

Sum up

I hope you liked Strongly Typed Identifiers. This concept can make your code cleaner and less error prone. Once you create such object, you can use it forever!

About the author

Programista lubiący ten fach. Połączenie perfekcjonisty i skauta z odrobiną eksperymentatora. Pracował dla dużych i małych, polskich i zagranicznych, prywatnych i publicznych podmiotów. Miał również przyjemność opiekować się praktykantami w jednej z poprzednich firm, jak również prowadzić ćwiczenia na Politechnice Warszawskiej, której jest dumnym absolwentem.

Comments

  1. Struck also has another big advantage, it uses less much less memory (Header for class would take more place than an int) and it doesn’t need GC to clean memory after class.

  2. I big fan of strongly typed identifires ! Another solution is to wrap this kind of method params AssignDuty(int customerId, int dutyId); to a class named Duty. Typed identifires are great for example endpoint input params binding. When you have an endpoint like /order/id, you can write custom modelbinder or type converter to bind this id to strongly typed identifires which is ofcourse less error prone. Another advantage to have class than struct its that struct have to have a default value. 0 for int for example and in some cases this is not good value of “nothing”. In most applications performance of struct vs class doesnt matter.

  3. This is cool in theory. However imagine that you have one hundred methods with at least two or three parameters. If you want to cover all of them with strong types, you will need three hundred additional classes (with not so simple logic inside) to maintain.
    Of course we can add some abstraction with default implementation, but still it will generate additional cost without really strong gain. You can achieve the same with well written integration tests.

    However, good post – it forced me to thing about the whole idea, even if from my angle it is not perfectly suitable. 🙂

    1. Hi, thanks for message.
      The future is not as bad as you described, in my opinion. Each strongly typed id class is needed for each identity you have, so usually it is not hundreds, but something about the number of Aggregate Roots you have (5, 10, 20…). Of course you will have hundreds of additional instances of strongly typed id classes, but that usually is not the main concern.
      Logic is strongly typed id class is rather simply. I prepared project TypedId for that (https://github.com/TomaszSitarek/TypedId), you can also get it as a NuGet (feel free to contribute!). I plan to describe that in one of the next posts, so stay tuned:)
      The integration tests are the good idea to deal with such complexity. Neverthless, strongly typed id’s may help as well. There are scenarios with passing integration tests, but ids are swapped.
      Of course, strongly typed id brings some overflow, and it wouldn’t be a good idea to use them everywhere. I think it is like with DDD – it should be used in most crucial parts of your bussiness.
      Thank you for your message again! Have a nice day!

Leave a Reply

Your email address will not be published. Required fields are marked *