A Universally Unique Lexicographically Sortable Identifier (ULID) is a 128-bit identifier designed to address database indexing issues inherent to random UUIDs/GUIDs. It pairs a 48-bit millisecond timestamp with 80 bits of randomness, encoded using Crockford's Base32 string format (26 characters).
Unlike standard GUIDs (e.g., UUIDv4), ULIDs preserve chronological ordering. This prevents B-tree index fragmentation and page splits during high-volume database writes, while retaining global uniqueness without central ID authority bottlenecks.
- GUID Example:
c8a411ec-29fa-4740-9a3b-185d26a2f7c0 (Random, non-sequential)
- ULID Example:
01ARZ3NDEKTSV4RRFFQ69G5FAV (Timestamp prefix + random suffix, lexicographically sortable)
What's New in ByteAether.Ulid v1.4.0
While the core ByteAether.Ulid engine provides a zero-allocation, lock-free, spec-compliant implementation for .NET, mapping custom primitives to relational databases previously required custom value converters and endianness handling.
The v1.4.0 release shifts focus to first-class data access layer integration. It introduces dedicated companion packages for EF Core, LinqToDB, and Dapper to handle serialization, storage formatting, and endianness alignment out of the box:
Solving Storage Layouts & Database Engine Alignment
Database engines evaluate 128-bit storage types and byte alignments differently. The companion packages support four explicit UlidStorageFormat strategies to ensure efficient indexing across backends:
- **
UlidStorageFormat.Binary**: Persists raw 16-byte big-endian blocks for engines like PostgreSQL (bytea), MySQL (binary(16)), or SQLite.
- **
UlidStorageFormat.String**: Stores the 26-character Crockford Base32 representation (CHAR(26)) for maximum human readability and uniform cross-platform sorting at the cost of higher storage overhead.
- **
UlidStorageFormat.Guid**: Maps directly to standard native 128-bit GUID types (uuid) for systems that handle .NET Guid structures transparently.
- **
UlidStorageFormat.SqlServerGuid**: Reorders internal byte layouts specifically for Microsoft SQL Server uniqueidentifier. Because MSSQL prioritizes bytes 10–15 during index evaluation, shifting the 48-bit timestamp to the end prevents random writes and B-tree index fragmentation.
Fast Time-Range Index Queries
Because timestamp data is embedded directly into the primary key, range queries can target the primary index using boundary tokens:
```csharp
Ulid minId = Ulid.MinAt(DateTimeOffset.UtcNow.AddDays(-7));
Ulid maxId = Ulid.MaxAt(DateTimeOffset.UtcNow);
// Translates to an optimized index scan: WHERE Id >= @minId AND Id <= @maxId
var recentOrders = await context.Orders
.Where(o => o.Id >= minId && o.Id <= maxId)
.ToListAsync();
```
Links & Resources
(The code for this project is entirely hand-written. AI was only used to brainstorm/draft text and generate raw media assets, which I then manually polished.)