Minid generates human-readable, URL-friendly, unique identifiers that are computed in-memory.
- Safe without encoding (uses only characters from ASCII)
- Avoids ambiguous characters (i/I/l/L/o/O/0)
- Easy for humans to read and pronounce
- Supports full UUID range (128 bits)
- Safe for URLs and file names
- Case-insensitive
- 30% smaller than Guid's default string format
- Supports formatting with prefixes
The Guid
8108afcc-980f-438d-bdd7-51375fcf073a
converted to a Minid Id
is encoded as 473cr1y0ghbyc3m1yfbwvn3nxx
.
https://dotnetfiddle.net/jim7YP
The motivation for this library came from a need to return readable "resource" identifiers in our APIs that could be computed quickly (no database lookups). In .NET the Guid Struct meets the uniqueness and computational requirements but its string representation is not optimal in terms of size and format.
With Base32 encoding we can encode a 128-bit Guid
into a 26-character string. My original implementation was based on a Base32 encoder ported from this Java implementation.
I later updated this to use Kristian Hellang's "CompactGuid" implementation which was far more performant and had better support for ambiguous characters 🙇♂️
dotnet add package minid
You can then use the provided Id
struct to generate identifiers in your applications.
public class Customer
{
public Id Id { get; }
public Customer()
{
Id = Id.NewId();
}
}
To get the Base32-encoded value call ToString()
on the Id
value:
string encoded = customer.Id.ToString();
You can also initialise the Id
type from an existing Guid:
var existingId = new Id(existingGuid);
To parse an encoded value:
if (Id.TryParse(encodedValue, out Id id))
{
}
A common pattern is to prefix API resource identifiers to indicate the resource type, for example cus_473cr1y0ghbyc3m1yfbwvn3nxx
to represent a customer identifier. This is particularly useful when an API needs to accept identifiers for multiple resource types so that it can handle the request accordingly.
To provide a prefix when generating a new Id
:
var prefixedId = Id.NewId(prefix: "cus");
When specifying a prefix, the format of the encoded Id
is {prefix}_{encoded_guid_value}
.
To parse an encoded value you can optionally specify the prefix you expect. This is slightly more performant since you can benefit from defining your prefixes as a constant, avoiding an allocation:
const string CustomerPrefix = "cus";
if (Id.TryParse(encodedValue, CustomerPrefix, out Id id))
{
}
If you don't provide a prefix and one exists, it will be detected by the presence of the _
separator, but no prefix validation will be performed.
The implicit detection is required when needing to convert the values using a TypeConverter
such as serializing and deserializing JSON (see below).
Note that currently the prefix is not considered when comparing two Id
values.
A TypeConverter
and System.Text.Json JsonConverter
are included so that you can bind, serialize and deserialize Id
values without explicit conversion.
Note that support for Type Converters was only added to Newtonsoft Json.NET from version 10.
We originally started to use Base32-encoding to format our API resource identifiers in responses. Internally we used the underlying Guid value. This became problematic when correlating client and internal requests. Effectively we had introduced a second implicit identifier and our codebase was littered with conversion to/from the encoded values.
In our case it was far better to use the same encoded representation throughout our platform. Given we were mostly using document/no-sql databases which typically store Guid values as strings, we benefited from the reduction in size.
If you don't want to depend on this type you can use string
but this will allocate more memory. My recommendation is to only convert to string
when you need to.
BenchmarkDotNet=v0.13.2, OS=macOS Monterey 12.5 (21G72) [Darwin 21.6.0]
Apple M1 Max, 1 CPU, 10 logical and 10 physical cores
.NET SDK=6.0.400
[Host] : .NET 6.0.8 (6.0.822.36306), Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 6.0.8 (6.0.822.36306), Arm64 RyuJIT AdvSIMD
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
NewId | 113.48 ns | 0.659 ns | 0.584 ns | - | - |
IdToString | 153.47 ns | 0.838 ns | 0.784 ns | 0.0381 | 80 B |
ParseId | 63.32 ns | 0.547 ns | 0.511 ns | - | - |
With prefixes:
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
NewPrefixedId | 111.78 ns | 0.437 ns | 0.387 ns | - | - |
PrefixedIdToString | 172.68 ns | 1.133 ns | 1.060 ns | 0.0420 | 88 B |
ParsePrefixedId | 65.65 ns | 0.597 ns | 0.558 ns | 0.0153 | 32 B |
ParseIdKnownPrefix | 55.82 ns | 0.266 ns | 0.249 ns | - | - |
Note that the NewPrefixedId
benchmark makes uses of a constant for the prefix, hence the zero allocation.