How to expose private ids without really exposing them

In a lots of project this is one of the questions that comes at the beginning: your entities are stored and have unique identifiers. The shorter the better for the database system but how to handle these keys when you expose an object? Do you expose the key? What if your key is deterministic like an integer? Do you add a public key with a random generation like a GUID? Or do you use a GUID as primay key?

Let's build a basic .NET 6 web API that exposes orders, using their integer unique identifier.

We have a basic Order object:


public class Order
{
    public int Id { get; init; }
    public string Article { get; init; }

    public Order(int id, string article)
    {
        Id = id;
        Article = article;
    }
}

And a simple service that have only one method that get an order by its id:


public class OrderService
{
    private static Order[] orders = new Order[3]
    {
        new Order(1, "Dell PC"),
        new Order(2, "Thinkpad PC"),
        new Order(3, "MacBook pro")
    };

    public Order Get(int id) => orders.SingleOrDefault(u => u.Id == id);
}

And finaly a controller that calls our service and exposes one method:


[Route("api/[controller]")]
[ApiController]
public class OrdersController : ControllerBase
{
    private readonly OrderService _orderService;

    public OrdersController(OrderService orderService)
    {
        _orderService = orderService;
    }

    [HttpGet("int/{id:int}")]
    public IActionResult Get([FromRoute] int id)
    {
        var order = _orderService.Get(id);
        if (order is null)
        {
            return NotFound();
        }

        return Ok(order);
    }
}

You can call the route /api/orders/int/1 that will return the data of the order #1:

The problem here is pretty obvious and a major security breach if you don't pay attention to this: if this url becomes public (like send in a mail to a customer or something), then a curious person could ask himself what the API ill do if the 1 in the url becomes a 2 or a 3 and the response is bad: it will return the detail of another order.

A common answer to this issue is to use as primary key or add as public key a unique identifier based on a non-deterministic format, ni .NET the GUID is perfect for that.

Let's add a public key to our order object and add an API endpoint to get an order by it:


public class Order
{
    public int Id { get; init; }
    public Guid PublicKey { get; init; }
    public string Article { get; init; }

    public Order(int id, Guid publicKey, string article)
    {
        Id = id;
        PublicKey = publicKey;
        Article = article;
    }
}

public class OrderService
{
    private static Order[] orders = new Order[3]
    {
        new Order(1, new Guid("795817f5-ad1c-486d-93ed-70107222ef7d"), "Dell PC"),
        new Order(2, new Guid("e3941adb-8731-4f74-93d6-e1bc3b1da5b5"), "Thinkpad PC"),
        new Order(3, new Guid("bf4bdb9d-25ad-48a1-9258-f52790afa39c"), "MacBook pro")
    };

    public Order Get(int id) => orders.SingleOrDefault(u => u.Id == id);

    public Order Get(Guid publicKey) => orders.SingleOrDefault(u => u.PublicKey == publicKey);
}

[Route("api/[controller]")]
[ApiController]
public class OrdersController : ControllerBase
{
    private readonly OrderService _orderService;

    public OrdersController(OrderService orderService)
    {
        _orderService = orderService;
    }

    [HttpGet("int/{id:int}")]
    public IActionResult Get([FromRoute] int id)
    {
        var order = _orderService.Get(id);
        if (order is null)
        {
            return NotFound();
        }

        return Ok(order);
    }

    [HttpGet("guid/{id:guid}")]
    public IActionResult Get([FromRoute] Guid id)
    {
        var order = _orderService.Get(id);
        if (order is null)
        {
            return NotFound();
        }

        return Ok(order);
    }
}

Now our order have a public key and we can get his detail using it:

Using a GUID or an UUID as primary key or another unique identifier is commonly used and effective but it's not perfect because of the GUID composition: a GUID is 16 byte long and can be longer in his string format so it's more data that your database will have to store and manipulate, your API too. Another tradeoff is that a GUID is not optimized for comparison so a search on a GUID column can be expensive if the schema is not well managed.

So is there a way to have sequential unique identifiers in our system but unguessable exposed ids without additional cost to our databse or our API? The answer is yes and has a name: Hashids.

Hashids is a small NuGet package that creates short unique unguessable string identifiers from integers. Identifiers generated with Hashids are pretty close to YouTube ones. This package has some options you can discover on its documentation like the possibility to use custom alphabet or to use longs and not integers. Also an important thing is that you will configure a custom salt so the generated ids will be specific to your project.

Let's use it in our API. We start by adding this package to our project:

Then we add the Hashids service in our controller:


public class OrdersController : ControllerBase
{
    private readonly OrderService _orderService;
    private readonly IHashids _hashids;

    public OrdersController(OrderService orderService, IHashids hashids)
    {
        _orderService = orderService;
        _hashids = hashids;
    }
}

Then we have to register the Hashids service in the Dependency Injection container, where we can configure our custom salt to generate custom encoded identifiers:


builder.Services.AddSingleton<IHashids>(_ => new Hashids(salt: "thisismyexamplesalt", minHashLength: 5));

Finaly we add a method that takes hashid is identifier and returns the detail of the order.

The Hashids method that convert an encoded string into a sequential integer is called Decode and returns an array because you can pass one or several encoded srings as argument, so you will have to handle this array thing.


[HttpGet("hashid/{id}")]
public IActionResult Get([FromRoute] string id)
{
    int[] rawId = _hashids.Decode(id);
    if (rawId.Length != 1)
    {
        return NotFound();
    }

    var order = _orderService.Get(rawId[0]);
    if (order is null)
    {
        return NotFound();
    }

    return Ok(order);
}

Now I can call my API endpoint to get an order by his hashid:

Using hashids as public identifier without storing them will allow you to save database memory and operations but the tradeoff, because there are always some, is that your API or your service will have to work a little more because it will have to call the Hashids service to encode and decode the ids.

Let's benchmark these two methods to have an idea of their runtime cost:


var benchmarkConfig = new ManualConfig()
                    .WithOptions(ConfigOptions.DisableOptimizationsValidator)
                    .AddValidator(JitOptimizationsValidator.DontFailOnError)
                    .AddLogger(ConsoleLogger.Default)
                    .AddColumnProvider(DefaultColumnProviders.Instance);
BenchmarkRunner.Run<HashidsBenchmark>(benchmarkConfig);

public class HashidsBenchmark
{
    private static readonly Hashids hashids = new ("testlab", 8);

    [Benchmark]
    public int IntFromHash()
    {
        return hashids.Decode("rBaLjAgw")[0];
    }

    [Benchmark]
    public string HashFromInt()
    {
        return hashids.Encode(1);
    }
}

This package could be optimized, for example by allowing us to encode and decode unique values so we won't have to handle arrays neither than the package, it could save some nanoseconds and bytes but as you see theses two operations are really efficients:

Hashids NuGet package benchmark on .NET 6

|      Method |     Mean |    Error |   StdDev | Allocated |
|------------ |---------:|---------:|---------:|----------:|
| IntFromHash | 20.98 us | 0.407 us | 0.515 us |     70 KB |
| HashFromInt | 13.38 us | 0.252 us | 0.449 us |     47 KB |

Note that we speak about microseconds and few kilobites here so even if you are calling these methods thousands of times it shouldn't hurt your application's performance.

Anyway I think Hashids is a really nice little NuGet package every .NET developer should know about it to have it in his toolbox.

July 03, 2022
  • .NET 6
  • Hashids
  • Hashids NuGet package