Have you ever requested an entity from a service (web API / database / generic service) and got a 404 / not found response when you're sure it exists? I've seen it quite a few times, and it sometimes comes down to requesting the entity using the wrong ID. In this post I show one way to avoid these sorts of errors by acknowledging the problem as primitive obsession, and using the C# type system to catch the errors for us.
Lots of people smarter than me have talked about primitive obsession in C#. In particular I found these resources by Jimmy Bogard, Mark Seemann, Steve Smith, and Vladimir Khorikov, as well as Martin Fowler's Refactoring book. I've recently started looking into F#, in which this is considered a solved problem as far as I can tell!
An example of the problem
To give some context, below is a very basic example of the problem. Imagine you have an eCommerce site, in which User
s can place an Order
. An Order
for this example is very basic, just the following few properties:
public class Order
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public decimal Total { get; set; }
}
You can create and read the Order
s for a User
using the OrderService
:
public class OrderService
{
private readonly List<Order> _orders = new List<Order>();
public void AddOrder(Order order)
{
_orders.Add(order);
}
public Order GetOrderForUser(Guid orderId, Guid userId)
{
return _orders.FirstOrDefault(
order => order.Id == orderId && order.UserId == userId);
}
}
This trivial implementation stores the Order
objects in memory, and has just two methods:
AddOrder()
: Add a newOrder
to the collectionGetOrderForUser()
: Get anOrder
with the givenId
andUserId
.
Finally, we have an API controller that can be called to create a new Order
or fetch an Order
:
[Route("api/[controller]")]
[ApiController, Authorize]
public class OrderController : ControllerBase
{
private readonly OrderService _service;
public OrderController(OrderService service)
{
_service = service;
}
[HttpPost]
public ActionResult<Order> Post()
{
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
var order = new Order { Id = Guid.NewGuid(), UserId = userId };
_service.AddOrder(order);
return Ok(order);
}
[HttpGet("{orderId}")]
public ActionResult<Order> Get(Guid orderId)
{
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
var order = _service.GetOrderForUser(userId, orderId);
if (order == null)
{
return NotFound();
}
return order;
}
}
This ApiController
is protected with an [Authorize]
attribute, so users have to be logged in to call it. It exposes two action methods:
Post()
: Used to create a newOrder
. The newOrder
object is returned in the response body.Get()
: Used to fetch an order with the provided ID. If found, theOrder
is returned in the response body.
Both methods need to know the UserId
for the currently logged in user, so they find the ClaimTypes.NameIdentifier
from the current User
claims and parse it into a Guid
.
Unfortunately, the code API controller above has a bug.
Did you spot it?
I don't blame you if not, I doubt I would.
The bug - All GUIDs are interchangeable
The code compiles and you can add a new Order
successfully, but calling Get()
always returns a 404 Not Found
.
The problem is on this line in OrderController.Get()
, where we're fetching the Order
from the OrderService
:
var order = _service.GetOrderForUser(userId, orderId);
The signature for that method is:
public Order GetOrderForUser(Guid orderId, Guid userId);
The userId
and orderId
arguments are inverted at the call site!
This example might seem a little contrived (requiring the userId
to be provided feels a bit redundant) but this general pattern is something you'll probably see in practice many times. Part of the problem is that we're using a primitive object (System.Guid
) to represent two different concepts: the unique identifier of a user, and the unique identifier of an order. The problem of using primitive values to represent domain concepts is called primitive obsession.
Primitive obsession
"Primitives" in this case refer to the built-in types in C#, bool
, int
, Guid
, string
etc. "Primitive obsession" refers to over-using these types to represent domain concepts that aren't a perfect fit. A common example might be a ZipCode
or PhoneNumber
field that is represented as a string
(or even worse, an int
!)
A string
might make sense initially, after all, you can represent a Zip Code as a string of characters, but there's a couple of problems with this.
First, by using a built-in type (string
), all the logic associated with the "Zip Code" concept must be stored somewhere external to the type. For example, only a limited number of string
values are valid Zip Codes, so you will no-doubt have some validation for Zip Codes in your app. If you had a ZipCode
type you could encapsulate all this logic in one place. Instead, by using a string
, you're forced to keep the logic somewhere else. That means the data (the ZipCode
field) and the methods for operating on it are separated, breaking encapsulation.
Secondly, by using primitives for domain concepts, you lose a lot of the benefits of the type system. C# won't let you do something like the following:
int total = 1000;
string name = "Jim";
name = total; // compiler error
But it has no problem with this, even though this would almost certainly be a bug:
string phoneNumber = "+1-555-229-1234";
string zipCode = "1000 AP"
zipCode = phoneNumber; // no problem!
You might think this sort of "mis-assignment" is rare, but a common place to find it is in methods that take multiple primitive objects as parameters. This was the problem in the original GetOrderForUser()
method.
So what's the solution to this primitive obsession?
The answer is encapsulation. Instead of using primitives, we can create custom types for each separate domain concept. Instead of a string
representing a Zip Code, create a ZipCode
class that encapsulates the concept, and use the ZipCode
type throughout your domain models and application.
Using strongly-typed IDs
So coming back to the original problem, how do we avoid the transposition error in GetOrderForUser
?
var order = _service.GetOrderForUser(userId, orderId);
By using encapsulation! Instead of using a Guid
representing the ID of a User
or the ID of an Order
, we can create strongly-typed IDs for both. So instead of a method signature like this:
public Order GetOrderForUser(Guid orderId, Guid userId);
You have a method like this (note the method argument types):
public Order GetOrderForUser(OrderId orderId, UserId userId);
An OrderId
cannot be assigned to a UserId
, and vice versa, so there's no way to call the GetOrderForUser
method with the arguments in the wrong order - it wouldn't compile!
So what do the OrderId
and UserId
types look like? That's up to you, but in the following section I show an example of one way you could implement them.
An implementation of OrderId
The following example is an implementation of OrderId
.
public readonly struct OrderId : IComparable<OrderId>, IEquatable<OrderId>
{
public Guid Value { get; }
public OrderId(Guid value)
{
Value = value;
}
public static OrderId New() => new OrderId(Guid.NewGuid());
public bool Equals(OrderId other) => this.Value.Equals(other.Value);
public int CompareTo(OrderId other) => Value.CompareTo(other.Value);
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is OrderId other && Equals(other);
}
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
public static bool operator ==(OrderId a, OrderId b) => a.CompareTo(b) == 0;
public static bool operator !=(OrderId a, OrderId b) => !(a == b);
}
OrderId
is implemented as a struct
here - it's a simple type that just wraps a Guid
, so a class
would probably be overkill. That said, if you're using an ORM like EF 6, using a struct
might cause you problems, so a class
might be easier. That also gives the option of creating a base StronglyTypedId
class to avoid some of the boiler plate.
There are some other potential issues with using a
stuct
, for example implicit parameterless constructors. Vladimir has a discussion about these problems here.
The only data in the type is held in the property, Value
, which wraps the original Guid
value that we were previously passing around. We have a single constructor that requires you pass in the Guid
value.
Most of the functions are overrides of the standard object
methods, and implementations of the IEquatable<T>
and IComparable<T>
methods to make it easier to work with the type. There's also overrides for the equality operators. I've written a few example tests demonstrating the type below.
Note, as Jared Parsons suggests in his recent post, I marked the
stuct
asreadonly
for performance reasons.. You need to be using C# 7.2 at least to usereadonly struct
.
Testing the strongly-typed ID behaviour
The following xUnit tests demonstrate some of the characteristics of the strongly-typed ID OrderId
. They also use a (similarly defined) UserId
to demonstrate that they are distinct types.
public class StronglyTypedIdTests
{
[Fact]
public void SameValuesAreEqual()
{
var id = Guid.NewGuid();
var order1 = new OrderId(id);
var order2 = new OrderId(id);
Assert.Equal(order1, order2);
}
[Fact]
public void DifferentValuesAreUnequal()
{
var order1 = OrderId.New();
var order2 = OrderId.New();
Assert.NotEqual(order1, order2);
}
[Fact]
public void DifferentTypesAreUnequal()
{
var userId = UserId.New();
var orderId = OrderId.New();
//Assert.NotEqual(userId, orderId); // does not compile
Assert.NotEqual((object) bar, (object) foo);
}
[Fact]
public void OperatorsWorkCorrectly()
{
var id = Guid.NewGuid();
var same1 = new OrderId(id);
var same2 = new OrderId(id);
var different = OrderId.New();
Assert.True(same1 == same2);
Assert.True(same1 != different);
Assert.False(same1 == different);
Assert.False(same1 != same2);
}
}
By using strongly-typed IDs like these we can take full advantage of the C# type system to ensure different concepts cannot be accidentally used interchangeably. Using these types in the core of your domain will help prevent simple bugs like incorrect argument order issues, which can be easy to do, and tricky to spot!
Unfortunately, it's not all sunshine and roses. You may be able to use these types in the core of your domain easily enough, but inevitably you'll eventually have to interface with the outside world. These days, that's typically via JSON APIs, often using MVC with ASP.NET Core. In the next post I'll show how to create some simple convertors to make working with your strongly-typed IDs simpler.
Summary
C# has a great type system, so we should use it! Primitive obsession is very common, but you should do your best to fight against it. In this post I showed a way to avoid issues with incorrectly using IDs by using strongly-typed IDs, so the type system can protect you. In the next post I'll extend these types to make them easier to use in an ASP.NET Core app.