While studying Domain Driven Design and software architecture, I’ve seen strong advocacy for the use of Strongly-Typed-Ids instead of the traditional use of strings, guids and integers with the aim of preventing ‘Primitive Obsession’ and the ensuing errors associated with this behaviour.

‘Primitive Obsession’ is a term coined by Martin Fowler, a prominent software developer and author. The term is used in software development to describe a scenario where developers use primitive data types to represent domain ideas.

Eric Evans, the author of the book “Domain-Driven Design: Tackling Complexity in the Heart of Software”, is one significant figure in the Domain-Driven Design (DDD) community, which often advocates for practices like strongly-typed IDs. While Evans himself may not explicitly advocate for strongly-typed IDs, the principles he outlines in his book align well with the concept.

Many advocates of statically-typed languages such as C#, Java, or TypeScript also tend to favor strongly-typed IDs. These languages, by their nature, encourage the use of explicit types to improve code clarity and safety.

In the context of EF Core applications, ‘Primitive Obsession’ often refers to using primitive types such as int, string, or Guid to represent entity IDs, rather than creating a separate, more specific type. For instance, rather than creating a CustomerId type for customer IDs, a developer might just use an int. This can lead to confusion as these IDs are passed around the application, as it’s unclear what type of entity the ID refers to.

While using primitive types can be quicker and simpler in the short term, it can lead to problems in the long term, including:

  • Lack of clarity: Without a specific type, it’s not immediately clear what an ID refers to. This can make the code harder to understand and maintain.
  • Type safety issues: If you’re using primitive types for IDs, it’s easy to accidentally pass an ID of one type to a method that expects an ID of a different type. The compiler won’t catch this error, and it could lead to bugs that are hard to track down.
  • Missed opportunities for encapsulation: By using a specific type, you can encapsulate logic related to the ID inside that type. For instance, if there are specific rules around what values an ID can have, those rules can be enforced in the ID type itself.

To counter this ‘Primitive Obsession’ one should use ‘Domain Primitives’ or ‘Value Objects’. These are small classes that encapsulate a simple domain concept. For example, instead of using an int for a customer ID, you could create a formal CustomerId class. This makes the code more self-documenting and helps prevent errors. However, it can increase the complexity of the codebase and requires careful design to ensure the classes are correctly implemented.

Lets look a little closer…

When building software applications, it’s common to use identifiers (IDs) to uniquely distinguish entities. This is especially true in the realm of databases where IDs are used as primary keys. In .NET’s Entity Framework Core, the default type for these identifiers is often an integer or a GUID. However, there’s a growing trend towards using Strongly-Typed IDs, an approach that has its own set of advantages and limitations.

What are Strongly-Typed IDs?

Traditionally, IDs in Entity Framework Core are represented as simple primitives, like int, long, or Guid. However, these types don’t provide any context or meaning about what they represent. They are just raw values.

Strongly-Typed IDs, on the other hand, wrap these raw values in a more meaningful type. For instance, instead of having an integer represent an OrderID, a new type OrderId is created. This new type can help to avoid errors and provide clarity about what the ID represents.

Example

public struct OrderId
{
    public int Value { get; }
    
    public OrderId(int value)
    {
        Value = value;
    }
}

In the example above the class name helps express the intent of the class, while being declared as a struct ensures that equality comparisons are based on their values. If a contained property were to be of a reference type however it would be necessary to override the Equals method (and GetHashCode method) in the struct to ensure a proper value comparison.

As of C# 9.0 as part of .NET 5, (released in November 2020), the newer record type permits a more compact expression of such types as shown:

public record OrderId(int Value);

The ease of declaration of such types makes it very tempting for use in new projects or retrofitting to older ones.

This being said, it would be wise to identify the full cost of the use of such types in any major project.