Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Using strongly-typed entity IDs to avoid primitive obsession (Part 1)

$
0
0
Using strongly-typed entity IDs to avoid primitive obsession (Part 1)

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 Users 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 Orders 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 new Order to the collection
  • GetOrderForUser(): Get an Order with the given Id and UserId.

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 new Order. The new Order object is returned in the response body.
  • Get(): Used to fetch an order with the provided ID. If found, the Order 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 as readonly for performance reasons.. You need to be using C# 7.2 at least to use readonly 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.


Viewing all articles
Browse latest Browse all 743

Trending Articles