vm2.Ulid
1.0.9
dotnet add package vm2.Ulid --version 1.0.9
NuGet\Install-Package vm2.Ulid -Version 1.0.9
<PackageReference Include="vm2.Ulid" Version="1.0.9" />
<PackageVersion Include="vm2.Ulid" Version="1.0.9" />
<PackageReference Include="vm2.Ulid" />
paket add vm2.Ulid --version 1.0.9
#r "nuget: vm2.Ulid, 1.0.9"
#:package vm2.Ulid@1.0.9
#addin nuget:?package=vm2.Ulid&version=1.0.9
#tool nuget:?package=vm2.Ulid&version=1.0.9
Universally Unique Lexicographically Sortable Identifier (ULID) for .NET
- Overview
- Short Comparison of ULID vs UUID (
System.Guid) - Prerequisites
- Install the Package (NuGet)
- Quick Start
- Get the Code
- Build from the Source Code
- Tests
- Benchmark Tests
- Build and Run the Example
- Basic Usage
- Why Do I Need
UlidFactory? - Performance
- Related Packages
- License
Overview
A small, fast, and spec-compliant .NET package that implements Universally Unique Lexicographically Sortable Identifier (ULID).
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 package exposes a vm2.Ulid value type and a vm2.UlidFactory for stable, monotonic generation.
Short Comparison of ULID vs UUID (System.Guid)
Universally unique lexicographically sortable identifiers (ULIDs) offer advantages over traditional globally unique identifiers (GUIDs, or UUIDs) in some scenarios:
- Lexicographic sorting: lexicographically sortable identifiers, useful for database indexing
- Timestamp component: most significant six bytes encode time, enabling chronological ordering
- Monotonic change: reduced fragmentation for high-frequency generation within the same millisecond
- Compact representation: 26-character Crockford Base32 string vs 36-character GUID hex with hyphens (8-4-4-4-12)
- Readable time hint: first 10 characters encode the timestamp; GUIDs do not expose creation time in a consistent way
- Binary compatibility: 128-bit values, easy integration with GUID-based systems
Prerequisites
- .NET 10.0 or later
Install the Package (NuGet)
Using the dotnet CLI:
dotnet add package vm2.UlidFrom Visual Studio Package Manager Console:
Install-Package vm2.Ulid
Quick Start
Install package
dotnet add package vm2.UlidGenerate ULID
using vm2; UlidFactory factory = new UlidFactory(); Ulid ulid = factory.NewUlid();
For testing, database seeding, and other automation, use the vm2.UlidTool CLI.
Get the Code
You can clone the GitHub repository. The project is in the src/UlidType directory.
Build from the Source Code
Command line:
dotnet build src/UlidType/UlidType.csprojVisual Studio:
- Open the solution and choose Build Solution (or Rebuild as needed).
Tests
The test project is in the test directory. It uses MTP v2 with xUnit v3.2.2. Compatibility varies by Visual Studio version.
Tests are buildable and runnable from the command line using the dotnet CLI and from Visual Studio Code across OSes.
Command line:
dotnet test --project test/UlidType.Tests/UlidType.Tests.csprojThe tests can also be run standalone after building the solution or the test project:
build the solution or the test project only:
dotnet build # build the full solution or dotnet build test/UlidType.Tests/UlidType.Tests.csproj # the test project onlyRun the tests standalone:
test/UlidType.Tests/bin/Debug/net10.0/UlidType.Tests
Benchmark Tests
The benchmark tests project is in the benchmarks directory. It uses BenchmarkDotNet v0.13.8. Benchmarks are buildable and
runnable from the command line using the dotnet CLI.
Command line:
dotnet run --project benchmarks/UlidType.Benchmarks/UlidType.Benchmarks.csproj -c ReleaseThe benchmarks can also be run standalone after building the benchmark project:
build the benchmark project only:
dotnet build -c Release benchmarks/UlidType.Benchmarks/UlidType.Benchmarks.csprojRun the benchmarks standalone (Linux/macOS):
benchmarks/UlidType.Benchmarks/bin/Release/net10.0/UlidType.BenchmarksRun the benchmarks standalone (Windows):
benchmarks/UlidType.Benchmarks/bin/Release/net10.0/UlidType.Benchmarks.exe
Build and Run the Example
The example is a file-based application GenerateUlids.cs in the examples directory. It demonstrates basic usage of the
vm2.Ulid library. The example is buildable and runnable from the command line using the dotnet CLI.
Command line:
dotnet run --file examples/GenerateUlids.csor just:
dotnet examples/GenerateUlids.csOn a Linux/macOS system with the .NET SDK installed, you can also run the example app directly:
examples/GenerateUlids.csProvided that:
- execute permission set
- first line ends with
\n(LF), not\r\n(CRLF) - no UTF-8 Byte Order Mark (BOM) at the beginning
These conditions can be met by running the following commands on a Linux system:
chmod u+x examples/GenerateUlids.cs dos2unix examples/GenerateUlids.cs
Basic Usage
using vm2;
// Recommended: reuse multiple UlidFactory instances, e.g. one per table or entity type.
// Ensures independent monotonicity per context.
UlidFactory factory = new UlidFactory();
Ulid ulid1 = factory.NewUlid();
Ulid ulid2 = factory.NewUlid();
// Default internal factory ensures thread safety and same-millisecond monotonicity across contexts.
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?
ULIDs must increase monotonically within the same millisecond. When multiple ULIDs are generated in a single millisecond, each subsequent ULID is greater by one in the least significant byte(s). A ULID factory tracks the timestamp and the last random bytes for each call. When the timestamp matches the previous generation, the factory increments the prior random part instead of generating a new random value.
The vm2.UlidFactory Class
The vm2.UlidFactory class encapsulates the requirements and exposes a simple interface for generating ULIDs. Use multiple
vm2.UlidFactory instances when needed, e.g. one per database table or entity type.
In simple scenarios, use the static method vm2.Ulid.NewUlid() instead of vm2.UlidFactory. It uses a single internal static
factory instance with a cryptographic random number generator.
ULID factories are thread-safe and ensure monotonicity of generated ULIDs across application contexts. The factory uses two providers: one for the random bytes and one for the timestamp.
Use dependency injection to construct the factory and manage the providers. DI keeps the provider lifetimes explicit, makes testing simple, and enforces a single, consistent configuration across the app or service.
Randomness Provider (vm2.IRandomNumberGenerator)
By default the vm2.UlidFactory uses a thread-safe, 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 purposes, for performance reasons, or if you are concerned about your source of entropy (/dev/random), 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.
Timestamp Provider (vm2.ITimestampProvider)
By default, the timestamp provider uses DateTime.UtcNow converted to Unix epoch time in milliseconds. If you need a different
source of time, e.g. for testing purposes, you can provide your own implementation of vm2.ITimestampProvider to the factory.
The UlidFactory in a Distributed System
In distributed database applications and services, ULIDs are often generated across many nodes. Design for collision avoidance and monotonicity from the start. Node-local monotonicity does not imply global monotonicity, and clock skew can surface quickly under load.
One approach uses a separate UlidFactory instance on each node with a unique node identifier. ULIDs remain distinct even when
generated in the same millisecond. However, global monotonicity across all nodes does not hold under this approach.
To maintain global monotonicity, a centralized ULID service can generate ULIDs for all nodes. This ensures uniqueness and monotonicity across the system, at the cost of a single point of failure and a potential performance bottleneck. Time synchronization across nodes remains a challenge; clock skew can cause non-monotonic ULIDs if not handled properly.
Another approach uses a consensus algorithm to coordinate ULID generation across nodes. This adds complexity and overhead.
The choice depends on system requirements and constraints. Consider trade-offs among uniqueness, monotonicity, performance, and complexity when designing a distributed ULID strategy.
Performance
Benchmark results vs similar Guid-generating functions, run on GitHub Actions:
BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.103
[Host] : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v3
DefaultJob : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v3
| Method | RandomProviderType | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|
| Ulid.NewUlid | CryptoRandom | 62.21 ns | 0.220 ns | 0.195 ns | 0.07 | 0.0024 | 40 B | NA |
| Factory.NewUlid | CryptoRandom | 62.66 ns | 0.399 ns | 0.373 ns | 0.07 | 0.0024 | 40 B | NA |
| Guid.NewGuid | N/A | 894.42 ns | 1.828 ns | 1.710 ns | 1.00 | - | - | NA |
| Factory.NewUlid | PseudoRandom | 61.87 ns | 0.247 ns | 0.219 ns | 0.07 | 0.0024 | 40 B | NA |
| Ulid.NewUlid | PseudoRandom | 62.04 ns | 0.441 ns | 0.413 ns | 0.07 | 0.0024 | 40 B | NA |
| Guid.NewGuid | N/A | 894.32 ns | 0.973 ns | 0.910 ns | 1.00 | - | - | NA |
| Ulid.Parse(StringUtf16) | N/A | 72.23 ns | 0.305 ns | 0.271 ns | 2.37 | 0.0024 | 40 B | NA |
| Ulid.Parse(StringUtf8) | N/A | 73.70 ns | 0.252 ns | 0.236 ns | 2.42 | 0.0024 | 40 B | NA |
| Guid.Parse(string) | N/A | 30.44 ns | 0.030 ns | 0.023 ns | 1.00 | - | - | NA |
| Guid.ToString | N/A | 17.94 ns | 0.398 ns | 0.333 ns | 1.00 | 0.03 | 96 B | 1.00 |
| Ulid.ToString | N/A | 61.42 ns | 1.154 ns | 1.080 ns | 3.42 | 0.08 | 192 B | 2.00 |
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)
random number generator on every call, whereas Ulid.NewUlid only uses it when the millisecond timestamp changes and if it
doesn't, it simply increments the random part of the previous call.
Related Packages
- ULID Specification - Official ULID spec
- vm2.UlidTool - ULID Generator Command Line Tool
License
MIT - See LICENSE
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Microsoft.Extensions.Configuration.Binder (>= 10.0.3)
- Microsoft.Extensions.Configuration.CommandLine (>= 10.0.3)
- Microsoft.Extensions.Configuration.EnvironmentVariables (>= 10.0.3)
- Microsoft.Extensions.Configuration.Json (>= 10.0.3)
- Microsoft.Extensions.DependencyInjection (>= 10.0.3)
- Microsoft.Extensions.Hosting (>= 10.0.3)
- Microsoft.Extensions.Logging (>= 10.0.3)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Logging.Configuration (>= 10.0.3)
- Microsoft.Extensions.Logging.Console (>= 10.0.3)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.3)
- Newtonsoft.Json (>= 13.0.4)
- System.Configuration.ConfigurationManager (>= 10.0.3)
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.9 | 102 | 2/16/2026 | |
| 1.0.8 | 102 | 2/16/2026 | |
| 1.0.8-preview.7 | 37 | 2/16/2026 | |
| 1.0.8-preview.5 | 41 | 2/15/2026 | |
| 1.0.7 | 119 | 2/13/2026 | |
| 1.0.6 | 124 | 2/9/2026 | |
| 1.0.5 | 144 | 2/9/2026 | |
| 1.0.4-preview.20251205.17 | 142 | 12/5/2025 | |
| 1.0.4-preview.20251030.16 | 172 | 10/30/2025 | |
| 1.0.4-preview.20251030.15 | 169 | 10/30/2025 | |
| 1.0.4-preview.20251029.14 | 170 | 10/29/2025 | |
| 1.0.4-preview.20251018.13 | 111 | 10/18/2025 | |
| 1.0.4-preview.20251007.12 | 163 | 10/7/2025 | |
| 1.0.3-preview.20250921.22 | 221 | 9/21/2025 |
1.1.1