In this article, we’ll explore why we might need sortable unique IDs in .NET and how to create them with the NewId NuGet Package.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in!

Why Might We Need Sortable Unique IDs in .NET

We all know there are two main approaches when it comes to generating IDs for entities in our projects: either an int or Guid (globally unique identifier) values. But both approaches have their problems. Let’s explore them in an API that deals with customer orders.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Integers as Primary Keys

First, let’s look at integers as primary keys:

public class Order
{
    public int Id { get; set; }
    public required string CustomerName { get; set; }
    public required IEnumerable<string> Products { get; set; }
    public required decimal TotalAmount { get; set; }
}

We create an Order class and use an int as the ID. 

With this approach, we leave the ID generation to the database provider. The good here is that we get short, nice, and consecutive IDs. But, with database-generated primary keys, we get one major downside – our applications get harder to scale up. Dealing with large amounts of insert statements forces our database to constantly use locks to deal with generating new primary keys.

Storing entities over several different databases and maintaining unique int IDs is also next to impossible. Another thing is that we might leak sensitive information to our competitors – do we want to have sequential IDs and let people know exactly how many orders we have?

Globally Unique Identifiers as Primary Keys

The other popular approach is to use Guid values:

public class OrderService(IUnitOfWork unitOfWork) : IOrderService
{
    public async Task<OrderDto> CreateAsync(
        OrderForCreationDto orderForCreationDto, 
        CancellationToken cancellationToken = default)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            CustomerName = orderForCreationDto.CustomerName,
            Products = orderForCreationDto.Products,
            TotalAmount = orderForCreationDto.TotalAmount,
        };

        unitOfWork.OrderRepository.Insert(order);
        await unitOfWork.SaveChangesAsync(cancellationToken);

        return new OrderDto
        {
            Id = order.Id,
            CustomerName = order.CustomerName,
            Products = order.Products,
            TotalAmount = order.TotalAmount,
        };
    }

    // ommited for brevity
}

We start by changing the Order‘s identification from int to Guid, and then we create our OrderService class. Our project is based on the Onion architecture, so our service takes in a Dto object, processes it, and inserts the entity into the database using a repository.

With this approach, the OrderService class is responsible for generating the primary key value. The biggest plus here is that we can use Guid.NewGuid() and get unique IDs with zero effort. Having unique, but random, IDs makes scaling our application to several databases very easy. But this is their greatest problem as well: it makes our data unsortable based on the primary keys alone and can lead to potential indexing problems. This type of primary key also takes up four times as much space as a regular integer when stored in the database.

And this is where the NewId library comes into play. By utilizing it, we combine the good sides of both int and Guid primary keys and eliminate some of the downsides.

What Is the NewId Library

The NewId library is a NuGet package that we can use to generate unique, yet sortable IDs. It is based on the now-retired Snowflake: an internal service at X (formerly Twitter) for generating sortable unique primary keys. NewId is part of the distributed application framework MassTransit and was developed to tackle problems present with both int and Guid identifiers. It’s aimed at providing a way to generate unique and sortable IDs at scale.

If you want to know more about the Masstransit framework and how we can use it you can check out our article Using MassTransit with RabbitMQ in ASP.NET Core.

The package generates IDs based on three things – a timestamp, a worker ID, and a process ID. This way we end up with unique IDs that are still sortable and won’t collide when we have multiple instances of our application and database.

Before we start generating IDs, we need to install the NewId package:

dotnet add package NewId

Using the dotnet add package command, we install the library.

Now that we have things ready, let’s start using the package to generate IDs.

How to Generate Sortable Unique IDs with the NewId Library in .NET

To generate our sortable unique IDs, we need to use the NewId class. It is located in the MassTransit namespace and has three methods. 

First, the Next() method – generates a new instance of the NewId class:

00070000-ac11-0242-3d9b-08dc45bed613
00070000-ac11-0242-c9d3-08dc45bed614
00070000-ac11-0242-cafd-08dc45bed614
00070000-ac11-0242-cb2e-08dc45bed614
00070000-ac11-0242-cb69-08dc45bed614

Next, the NextGuid() method – generates a new Guid value:

00070000-ac11-0242-df20-08dc45bed614
00070000-ac11-0242-0c11-08dc45bed615
00070000-ac11-0242-0d30-08dc45bed615
00070000-ac11-0242-0d46-08dc45bed615
00070000-ac11-0242-0d58-08dc45bed615

Finally, the NextSequentialGuid() method – generates a new sequential Guid value:

08dc45be-d615-19b5-0242-ac1100070000
08dc45be-d615-1b01-0242-ac1100070000
08dc45be-d615-1b46-0242-ac1100070000
08dc45be-d615-1b66-0242-ac1100070000
08dc45be-d615-1ba4-0242-ac1100070000

We can see that with the Next() and NextGuid() methods, we get the same pattern where the NextSequentialGuid() method has a slightly different pattern. The latter two methods return a Guid value and no change will be needed to our class, but if we opt for the Next() method, we’ll have to change the ID type of our Order class.

Let’s use one of them:

public class OrderService(IUnitOfWork unitOfWork) : IOrderService
{
    public async Task<OrderDto> CreateAsync(
        OrderForCreationDto orderForCreationDto, 
        CancellationToken cancellationToken = default)
    {
        var order = new Order
        {
            Id = NewId.NextSequentialGuid(),
            CustomerName = orderForCreationDto.CustomerName,
            Products = orderForCreationDto.Products,
            TotalAmount = orderForCreationDto.TotalAmount,
        };

        unitOfWork.OrderRepository.Insert(order);
        await unitOfWork.SaveChangesAsync(cancellationToken);

        return new OrderDto
        {
            Id = order.Id,
            CustomerName = order.CustomerName,
            Products = order.Products,
            TotalAmount = order.TotalAmount,
        };
    }

    // ommited for brevity
}

In our OrderService class, where we generate the IDs, we replace the Guid.NewGuid() method with NewId.NextSequentialGuid() method. This one thing is the only change we need to do.

Let’s run our API and add some orders:

[
  {
    "id": "08dc45c0-b5b5-0d5d-5811-22b038790000",
    "customerName": "Marcel Waters",
    "products": [
      "Piano"
    ],
    "totalAmount": 599.99
  },
  {
    "id": "08dc45c0-d6f4-fbed-5811-22b038790000",
    "customerName": "Elizabeth Doyle",
    "products": [
      "Vase",
      "Mirror",
      "Blanket"
    ],
    "totalAmount": 49.39
  },
  {
    "id": "08dc45c0-f143-a9f9-5811-22b038790000",
    "customerName": "Rayford Lopez",
    "products": [
      "Headphones",
      "Microphone"
    ],
    "totalAmount": 86.06
  }
]

We can see that our entities now have unique yet not completely random identifiers that can be sorted.

When Not to Use the NewId Library to Generate Sortable Unique IDs

While the NewId library offers us the convenience of generating sortable unique IDs, there are a few scenarios where we should avoid using them. It’s crucial to keep in mind that the IDs we generate have a level of predictability. They can be guessed when you know the algorithm they are created with.

Therefore, it’s a good idea to not use NewId-generated IDs in contexts where unpredictability and security are paramount. We shouldn’t use such IDs for sensitive information such as passwords, security tokens, or any other data requiring high levels of confidentiality. If we rely on the NewId library in such scenarios we could potentially compromise the security of our applications. This can expose them to vulnerabilities and unauthorized access. Therefore, it’s vital to be cautious when security is critical in our applications.

Conclusion

In this article, we explored the limitations of both integer and globally unique identifiers when used as primary keys and the need for a new approach. The NewId NuGet Package provides us with a solution by offering unique, sortable IDs based on a combination of timestamp, worker ID, and process ID. With the NewId library, we gain the benefits of both int and Guid primary keys while reducing their drawbacks. This enables us to easily achieve scalability, and efficient indexing, as well as improve our data organization. All of this ultimately enhances the robustness and functionality of our applications.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!