Blog
How to expose private ids without really exposing them
In a lot of projects this is one of the questions that comes up at the beginning: your entities are stored and have unique identifiers. The shorter the better for the database system, but how do you 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 the primary 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 has only one method, which gets 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 finally 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 it: if this URL becomes public (for example sent in an email to a customer), then a curious person could wonder what the API will do if the 1 in the URL becomes a 2 or a 3, and the answer is bad: it will return the details of another order.

A common answer to this issue is to use as a primary key, or to add as a public key, a unique identifier based on a non-deterministic format; in .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 has a public key and we can get its details using it:

Using a GUID or a UUID as a primary key or as another unique identifier is common and effective, but it's not perfect because of the GUID composition: a GUID is 16 bytes long and can be longer in its string format, so it's more data that your database will have to store and manipulate, and 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 in its documentation, like the possibility to use a custom alphabet or to use longs instead of integers. Another 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));
Finally we add a method that takes a hashid as identifier and returns the details of the order.
The Hashids method that converts an encoded string into a sequential integer is called Decode and returns an array, because you can pass one or several encoded strings as arguments, 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 its hashid:

Using hashids as public identifiers without storing them will allow you to save database memory and operations, but the tradeoff, because there is always one, 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 single values so that neither we nor the package would have to handle arrays; it could save some nanoseconds and bytes, but as you can see these two operations are really efficient:

| 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 are talking about microseconds and a few kilobytes 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 that every .NET developer should know about, to have it in their toolbox.