r/csharp • u/GigAHerZ64 • 4h ago
Showcase Streamlining .NET Data Layers with ByteAether.Ulid v1.4.0 (EF Core, LinqToDB, Dapper Support)
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:
- ByteAether.Ulid.EntityFrameworkCore: Provides global model conventions (
RegisterUlid(...)) and explicit fluent property converters forUlidandUlid?. - ByteAether.Ulid.linq2db: Integrates directly into the
DataOptionsbuilder pipeline. - ByteAether.Ulid.Dapper: Registers global type handlers (
DapperUlid.RegisterUlid(...)) for query parameterization.
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 .NETGuidstructures transparently. - **
UlidStorageFormat.SqlServerGuid**: Reorders internal byte layouts specifically for Microsoft SQL Serveruniqueidentifier. 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
- Release Blog Post: Streamlining .NET Data Layers with ByteAether.Ulid v1.4.0
- Core Library (NuGet): ByteAether.Ulid
- GitHub Repository: ByteAether/Ulid
(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.)