Skip to content

Minid generates human-readable, URL-friendly, unique identifiers that are computed in-memory.

License

Notifications You must be signed in to change notification settings

benfoster/minid

Repository files navigation

Minid

NuGet NuGet License

Build Coverage Status Quality Gate Status GuardRails badge

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

Example

The Guid 8108afcc-980f-438d-bdd7-51375fcf073a converted to a Minid Id is encoded as 473cr1y0ghbyc3m1yfbwvn3nxx.

https://dotnetfiddle.net/jim7YP

Motivation

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 🙇‍♂️

Usage

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))
{

}

Prefixes

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.

Serialization and Model-binding

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.

Why a dedicated type?

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.

Benchmarks

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.

About

Minid generates human-readable, URL-friendly, unique identifiers that are computed in-memory.

Topics

Resources

License

Stars

Watchers

Forks

Packages