vm2.Ulid 1.0.3-preview.20250921.22

This is a prerelease version of vm2.Ulid.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package vm2.Ulid --version 1.0.3-preview.20250921.22
                    
NuGet\Install-Package vm2.Ulid -Version 1.0.3-preview.20250921.22
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="vm2.Ulid" Version="1.0.3-preview.20250921.22" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="vm2.Ulid" Version="1.0.3-preview.20250921.22" />
                    
Directory.Packages.props
<PackageReference Include="vm2.Ulid" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add vm2.Ulid --version 1.0.3-preview.20250921.22
                    
#r "nuget: vm2.Ulid, 1.0.3-preview.20250921.22"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package vm2.Ulid@1.0.3-preview.20250921.22
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=vm2.Ulid&version=1.0.3-preview.20250921.22&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=vm2.Ulid&version=1.0.3-preview.20250921.22&prerelease
                    
Install as a Cake Tool

Universally Unique Lexicographically Sortable Identifier (ULID) for .NET

Overview

A small, fast, and spec-compliant ULID (Universally Unique Lexicographically Sortable Identifier) implementation for .NET.

ULIDs combine a 48-bit timestamp (milliseconds since Unix epoch) with 80 bits of randomness, producing compact 128-bit identifiers that are lexicographically sortable by creation time.

This repository exposes a vm2.Ulid value type and an vm2.UlidFactory for stable, monotonic generation.

Short Comparison of ULID vs UUID (System.Guid)

The universaly unique lexicographically sortable identifiers (ULIDs) offer some advantages over the traditional universally unique identifiers (UUIDs or GUIDs) in certain scenarios:

  • Lexicographical Sorting: ULIDs are designed to be lexicographically sortable, which may be beneficial for database indexing and retrieval
  • Timestamp Component: the most significant six bytes of the ULIDs are a timestamp component, allowing for chronological ordering of the identifiers
  • Change monotonically: this can improve performance in certain database scenarios and cause less fragmentation. For identifiers generated in quick succession (within the same millisecond) the random component monotonically increments
  • Compact Representation: the canonical representation of ULIDs is more compact compared to UUIDs: they are represented as a 26-character, case-insensitive string (Base32), while UUIDs are typically represented as a 36 hexadecimal characters, divided into five groups separated by hyphens, following the pattern: 8-4-4-4-12
  • ULIDs are binary-compatible with UUIDs, allowing for easy integration in systems that already use UUIDs

Prerequisites

  • .NET 9.0 or later

Basic Usage

using vm2;

// Recommended: create and reuse more than one UlidFactory-ies, e.g. one per DB table or entity type which require ULIDs.
// This ensures that ULIDs generated in different contexts have their own monotonicity guarantees.

UlidFactory factory = new UlidFactory();

Ulid ulid1 = factory.NewUlid();
Ulid ulid2 = factory.NewUlid();

// Using the default internal factory ensures thread-safety and monotonicity within the same millisecond for ULIDs generated in
// different contexts of the app or service.

Ulid ulid = Ulid.NewUlid();

Debug.Assert(ulid1 != ulid2);                           // uniqueness
Debug.Assert(ulid1 < ulid2);                            // comparable
Debug.Assert(ulid  > ulid2);                            // comparable

var ulid1String = ulid1.String();                       // get the ULID canonical string representation
var ulid2String = ulid1.String();

Debug.Assert(ulid1String != ulid2String);               // ULID strings are unique
Debug.Assert(ulid1String < ulid2String);                // ULID strings are lexicographically sortable
Debug.Assert(ulid1String.Length == 26);                 // ULID string representation is 26 characters long

Debug.Assert(ulid1 <= ulid2);
Debug.Assert(ulid1.Timestamp < ulid2.Timestamp ||       // ULIDs are time-sortable and the timestamp can be extracted
             ulid1.Timestamp == ulid2.Timestamp &&      // if generated in the same millisecond
             ulid1.RandomBytes != ulid2.RandomBytes);   // the random parts are guaranteed to be different

Debug.Assert(ulid1.RandomBytes.Length == 10);           // ULID has 10 bytes of randomness

Debug.Assert(ulid1.Bytes.Length == 16);                 // ULID is a 16-byte (128-bit) value

var ulidGuid  = ulid1.ToGuid();                         // ULID can be converted to Guid
var ulidFromGuid = new Ulid(ulidGuid);                  // ULID can be created from Guid

var ulidUtf8String = Encoding.UTF8.GetBytes(ulid1String);

Ulid.TryParse(ulid1String, out var ulidCopy1);           // parse ULID from UTF-16 string (26 UTF-16 characters)
Ulid.TryParse(ulidUtf8String, out var ulidCopy2);       // parse ULID from its UTF-8 string (26 UTF-8 characters/bytes)

Debug.Assert(ulid1 == ulidCopy1 &&                      // Parsed ULIDs are equal to the original
             ulid1 == ulidCopy2);

Why do I need UlidFactory?

One of the requirements for the ULIDs is that they increase monotonically within the same millisecond. This means that if you generate multiple ULIDs in the same millisecond, each subsequent ULID must be greater than the previous one by one in the least significant byte(s). To ensure that, there must be a ULID generating object (factory) that keeps track of the timestamp and the last generated random bytes. If during generation of a ULID the factory finds out that the current timestamp is the same as the timestamp of the previous generation to the millisecond, the factory must increment the previous random part and use it, rather than generating new randomness.

The UlidFactory class encapsulates this logic, providing a simple interface for generating ULIDs that meet this requirement.

It is prudent to create and reuse more than one UlidFactory instances, e.g. one per DB table or entity type which require ULIDs. The ULID factory(s) are thread-safe and ensure monotonicity of the generated ULIDs in different contexts of an application or a service.

By default the UlidFactory uses a cryptographic random number generator (vm2.UlidRandomProviders.CryptoRandom), which is suitable for most applications. If you need a different source of randomness (e.g. for testing or performance reasons) or you are concerned about the entropy source, you can explicitly specify that the factory should use the pseudo-random number generator vm2.UlidRandomProviders.PseudoRandom. You can also provide your own, thread-safe implementation of vm2.IRandomNumberGenerator to the factory.

In contrast, if you do not want to use the vm2.UlidFactory directly, you can use the static vm2.Ulid.NewUlid() method, which uses an internal static factory instance with a cryptographic random number generator.

Get the code

Clone this repository:

git clone https://github.com/vmelamed/vm2.Ulid.git
cd vm2.Ulid

Install the package (NuGet)

If a NuGet package is published for this project you can add it

  • with the dotnet CLI:

    dotnet add vm2.Ulid package vm2.Ulid
    
  • Or from Visual Studio use the Package Manager Console:

    Install-Package vm2.Ulid
    

Build from source

  • Command line:

    dotnet build src/UlidType/UlidType.csproj
    
  • Visual Studio:

    • Open the solution and choose Build Solution (or use Rebuild as needed).

Run the example:

dotnet run --project examples/GenerateUlids/GenerateUlids.csproj

Performance

You can build and run the benchmark tests in release mode with:

benchmarks/UlidType.Benchmarks/bin/Release/net9.0/UlidType.Benchmarks.exe --filter * --memory --artifacts ../../../BenchmarkDotNet.Artifacts

Here are some benchmark results with similar Guid functions as baselines from GitHub Actions:

BenchmarkDotNet v0.15.3, Linux Ubuntu 24.04.3 LTS (Noble Numbat)<br/> AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores .NET SDK 9.0.305<br/>

  • [Host] : .NET 9.0.9 (9.0.9, 9.0.925.41916), X64 RyuJIT x86-64-v3
  • DefaultJob : .NET 9.0.9 (9.0.9, 9.0.925.41916), X64 RyuJIT x86-64-v3
Method Mean Error StdDev Ratio Gen0 Allocated Alloc Ratio RandomProviderType
UlidFactory.NewUlid 56.12 ns 0.085 ns 0.071 ns 0.09 0.0024 40 B NA CryptoRandom
Ulid.NewUlid 56.49 ns 0.105 ns 0.098 ns 0.09 0.0024 40 B NA CryptoRandom
Guid.NewGuid[^1] 595.28 ns 1.149 ns 0.897 ns 1.00 - - NA CryptoRandom
UlidFactory.NewUlid 55.99 ns 0.106 ns 0.094 ns 0.09 0.0024 40 B NA PseudoRandom
Ulid.NewUlid 56.24 ns 0.140 ns 0.131 ns 0.09 0.0024 40 B NA PseudoRandom
Guid.NewGuid 595.64 ns 0.416 ns 0.368 ns 1.00 - - NA PseudoRandom
Guid.Parse 30.16 ns 0.064 ns 0.060 ns 1.00 - - NA
Ulid.ParseUtf8String 77.28 ns 0.256 ns 0.239 ns 2.56 0.0024 40 B NA
Ulid.ParseString 80.95 ns 0.300 ns 0.266 ns 2.68 0.0024 40 B NA
Guid.ToString 16.38 ns 0.087 ns 0.082 ns 1.00 0.0057 96 B 1.00
Ulid.ToString 47.93 ns 0.249 ns 0.221 ns 2.93 0.0048 80 B 0.83

Legend:

  • Mean : Arithmetic mean of all measurements
  • Error : Half of 99.9% confidence interval
  • StdDev : Standard deviation of all measurements
  • Ratio : Mean of the ratio distribution ([Current]/[Baseline])
  • RatioSD : Standard deviation of the ratio distribution ([Current]/[Baseline])
  • Gen0 : GC Generation 0 collects per 1000 operations
  • Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  • 1 ns : 1 Nanosecond (0.000000001 sec)

[^1]: Guid.NewGuid is ~10 times slower than Ulid.NewUlid because it uses a cryptographic random number generator on every call, whereas Ulid.NewUlid only uses it when the millisecond timestamp changes and if it doesn't, it just increments the previous random part.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.4-alpha.0.2 113 9/24/2025
1.0.3-preview.20250921.22 175 9/21/2025
1.0.3-preview.20250921.21 171 9/21/2025
1.0.2 186 9/20/2025
1.0.2-preview.20250920.20 153 9/20/2025
1.0.0 412 9/19/2025

Initial version of the vm2.Ulid.