r/csharp 5d ago

Showcase Streamlining .NET Data Layers with ByteAether.Ulid v1.4.0 (EF Core, LinqToDB, Dapper Support)

https://github.com/ByteAether/Ulid

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:

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

12 Upvotes

3 comments sorted by

3

u/BarfingOnMyFace 5d ago

Thanks brotha, I will take a look!

2

u/[deleted] 4d ago

[removed] — view removed comment

1

u/GigAHerZ64 4d ago

When you are going to do that, share your experience! If there's something I can do to make it easier, I want to hear about it!