You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
During high-throughput transaction bursts within the same millisecond, the 80-bit random component of a ULID can saturate. Traditional libraries respond to this saturation by throwing an `OverflowException` to protect strict timestamp boundaries. ByteAether.Ulid introduces a non-blocking alternative: when the 80-bit random segment saturates during a high-throughput burst within a single millisecond, it gracefully increments the millisecond timestamp component instead of throwing. This ensures uninterrupted ID generation under extreme local load.
32
+
During high-throughput transaction bursts within the same millisecond, the 80-bit random component of a ULID can saturate. Traditional libraries respond to this saturation by throwing an `OverflowException` to protect strict timestamp boundaries. ByteAether.Ulid introduces a non-blocking alternative: when the 80-bit random segment saturates during a high-throughput burst within a single millisecond, it gracefully increments the millisecond timestamp component instead of throwing. This ensures uninterrupted ID generation under extreme local loads.
33
33
34
34
While this introduces a micro-scale timestamp adjustment localized strictly to the executing instance, the system clock catches up immediately once the burst subsides. The drift remains well within standard network latency boundaries and aligns with the workarounds in [ULID specification issue #39](https://github.com/ulid/spec/issues/39#issuecomment-2252145597).
35
35
36
36
### Mitigating Enumeration Attacks
37
37
38
-
Monotonic identifiers generated in rapid succession can expose predictable sequences, leaving systems vulnerable to enumeration attacks. This library mitigates this by supporting configurable random increments (ranging from 1-byte to 4-bytes) to the random component, as discussed in [ULID specification issue #105](https://github.com/ulid/spec/issues/105). This preserves strict lexicographical sortability while ensuring cryptographic unpredictability.
38
+
Monotonic identifiers generated in rapid succession can expose predictable sequences, leaving systems vulnerable to enumeration attacks. This library mitigates this risk by supporting configurable random increments (ranging from 1 to 4bytes) applied to the random component, as discussed in [ULID specification issue #105](https://github.com/ulid/spec/issues/105). This preserves strict lexicographical sortability while ensuring cryptographic unpredictability.
39
39
40
40
### ULID vs UUIDv7
41
41
42
42
While modern standards like UUIDv7 introduce timestamp-based sorting, [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#name-monotonicity-and-counters) treats sub-millisecond monotonicity as optional. [The native .NET UUIDv7 provider (`Guid.CreateVersion7`)](https://github.com/dotnet/runtime/blob/571b044582ceb7fe426b7f143c703064aa9ea4db/src/libraries/System.Private.CoreLib/src/System/Guid.cs#L306) uses random bits within the sub-millisecond payload rather than a strict sequential counter, sacrificing true chronological ordering under heavy bursts.
43
43
44
-
Furthermore, using .NET's native `Guid` structures for sequential IDs introduces severe endianness conflicts. Because `System.Guid` utilizes a legacy mixed-endian internal structure, most standard database providers serialize this raw memory layout directly to disk without adjustment. This scrambles the big-endian timestamp layout, completely breaking chronological index sorting. For engines with highly rigid index layouts like Microsoft SQL Server, time-first structures natively conflict with [custom `uniqueidentifier` indexing order](https://learn.microsoft.com/en-us/dotnet/api/system.data.sqltypes.sqlguid.compareto?view=net-10.0#remarks), triggering catastrophic page fragmentation.
44
+
Furthermore, using .NET's native `Guid` structures for sequential IDs introduces severe endianness conflicts. Because `System.Guid` utilizes a legacy mixed-endian internal structure, most database providers serialize this raw memory layout directly to disk without modification. This scrambles the big-endian timestamp layout, completely breaking chronological index sorting. For engines with highly rigid index layouts like Microsoft SQL Server, time-first structures natively conflict with [custom `uniqueidentifier` indexing order](https://learn.microsoft.com/en-us/dotnet/api/system.data.sqltypes.sqlguid.compareto?view=net-10.0#remarks), triggering catastrophic page fragmentation.
45
45
46
46
**ByteAether.Ulid** corrects this by mandating big-endian, strict lexicographical sortability directly at the specification level. It features optimized storage strategies (`String`, `Binary`, `Guid`, and `SqlServerGuid`) across major ORMs to maintain perfect index allocations and deterministic sorting whether targeting PostgreSQL, MS SQL Server, MySQL, or SQLite.
47
47
@@ -64,7 +64,7 @@ This library explicitly **multi-targets** each runtime version listed below, ena
-**Specification-Compliant**: Fully adheres to the ULID specification.
66
66
-**Interoperable**: Includes conversion methods to and from GUIDs, [Crockford's Base32](https://www.crockford.com/base32.html) strings, and byte arrays.
67
-
-**Ahead-of-Time (AOT) Compilation Compatible**: Fully compatible with AOT compilation for improved startup performance and smaller binary sizes.
67
+
-**Ahead-of-Time (AOT) Compilation**: Fully compatible with Native AOT for improved startup performance and smaller binary footprints.
68
68
-**Error-Free Generation**: Prevents `OverflowException` by incrementing the timestamp component when the random part overflows, ensuring continuous unique ULID generation.
Because ULIDs embed a millisecond-precision timestamp and maintain lexicographical order, you can use `Ulid.MinAt()` and `Ulid.MaxAt()` to generate boundary instances for specific time windows. This approach provides a uniform mechanism for range filtering across both in-memory collections and abstract data layers:
119
119
120
120
```csharp
121
-
// Define the temporal boundaries of your window
121
+
// Define temporal boundaries for the target window
@@ -137,7 +137,7 @@ var query = "SELECT * FROM Records WHERE Id >= @Min AND Id <= @Max";
137
137
> [!IMPORTANT]
138
138
> **Database Persistence Considerations**
139
139
>
140
-
> While range evaluations remain consistent for in-memory object graphs, executing these queries against a relational database introduces critical persistence dependencies:
140
+
> While range evaluations remain consistent across in-memory object graphs, executing these queries against a relational data store introduces critical persistence dependencies:
141
141
> ***Storage Format & Byte Order**: Certain database engines and native UUID data types utilize mixed-endian byte layouts. If a ULID is persisted using a strategy that reorders its raw big-endian bytes, chronological sorting behavior will diverge between the application and the database server.
142
142
> ***Index & Query Integrity**: Mismatches between the database engine's native sorting rules and the chosen storage format can result in broken data retrieval, bypassed indexes, or incorrect query results during database-side range operations (`>=`, `<=`) and `ORDER BY` execution.
143
143
>
@@ -208,7 +208,7 @@ The `Ulid` implementation provides the following properties and methods:
208
208
-`Ulid.New(ReadOnlySpan<byte> bytes)`\
209
209
Creates a ULID from an existing byte array.
210
210
-`Ulid.New(Guid guid)`\
211
-
Create from an existing `Guid`.
211
+
Creates a ULID from an existing `Guid`.
212
212
-`Ulid.MinAt(DateTimeOffset datetime)`\
213
213
Creates the minimum possible ULID value for the specified `DateTimeOffset`.
214
214
-`Ulid.MinAt(long timestamp)`\
@@ -221,11 +221,11 @@ The `Ulid` implementation provides the following properties and methods:
221
221
### Checking Validity
222
222
223
223
-`Ulid.IsValid(string ulidString)`\
224
-
Validates if the given string is a valid ULID.
224
+
Validates whether the specified string represents a valid ULID.
225
225
-`Ulid.IsValid(ReadOnlySpan<char> ulidString)`\
226
-
Validates if the given span of characters is a valid ULID.
226
+
Validates whether the specified span of characters represents a valid ULID.
227
227
-`Ulid.IsValid(ReadOnlySpan<byte> ulidBytes)`\
228
-
Validates if the given byte array represents a valid ULID.
228
+
Validates whether the specified byte array represents a valid ULID.
229
229
230
230
### Parsing
231
231
@@ -251,7 +251,7 @@ The `Ulid` implementation provides the following properties and methods:
251
251
-`.Time`\
252
252
Gets the timestamp component of the ULID as a `DateTimeOffset`.
253
253
-`.TimeBytes`\
254
-
Gets the time component of the ULID as a `ReadOnlySpan<byte>`.
254
+
Gets the timestamp component of the ULID as a `ReadOnlySpan<byte>`.
255
255
-`.Random`\
256
256
Gets the random component of the ULID as a `ReadOnlySpan<byte>`.
257
257
@@ -329,6 +329,7 @@ To seamlessly use ULIDs with [Entity Framework Core](https://github.com/dotnet/e
Register the ULID conventions within your `DbContext` via the `ConfigureConventions` method. You can choose from various underlying storage strategies (`String`, `Binary`, `Guid`, or `SqlServerGuid`):
> Dapper maps .NET types globally via a 1:1 scheme (`Type` → `TypeHandler`). You must choose a single global storage strategy for your entire application lifecycle. Mixing different formats (e.g., `String` and `Binary`) across distinct tables within the same runtime instance is not supported.
445
+
> Dapper maps .NET types globally using a 1:1 scheme (`Type` → `TypeHandler`). A single global storage strategy must be selected for the entire application lifecycle. Mixing formats (e.g., `String` and `Binary`) across distinct tables within the same process instance is not supported.
444
446
445
447
More details in the package's [PACKAGE.md](./src/Dapper/PACKAGE.md) file.
446
448
@@ -450,20 +452,30 @@ To use ULIDs with **Newtonsoft.Json**, you need to create a custom **JsonConvert
450
452
451
453
#### 1. Create the Custom JsonConverter
452
454
453
-
First, create a custom `JsonConverter` for `Ulid` to serialize and deserialize it as a `string`:
455
+
First, create a custom `JsonConverter<Ulid>` for `Ulid` to handle string serialization and deserialization:
Alternative .NET ecosystem solutions exhibit design constraints or spec deviations under heavy production loads:
627
645
628
646
1.`NetUlid`: Monotonicity guarantees are thread-confined and fail across concurrent multi-threaded execution loops.
629
-
2.`NUlid`: While providing a monotonic random provider (`MonotonicUlidRng`), it does not offer automated, out-of-the-box global state management. Developers must manually instantiate and persist the generator across calls, requiring custom generation wrappers to maintain thread-safe monotonicity in practice.
647
+
2.`NUlid`: Although it provides a monotonic random provider (`MonotonicUlidRng`), it lacks out-of-the-box global state management. Developers must manually instantiate and persist the generator instance, requiring custom wrappers to maintain thread-safe monotonicity across call sites.
630
648
3.`Ulid` (Cysharp) & `GuidV7`: Do not implement monotonicity.
631
-
4.`Ulid` (Cysharp): Relies on a cryptographically non-secure`XOR-Shift64` algorithm for sequence generation after seeding.
649
+
4.`Ulid` (Cysharp): Relies on a cryptographically insecure`XOR-Shift64` algorithm for sequence generation after seeding.
632
650
5. Native `Guid` / `GuidV7`: [Microsoft documentation explicitly warns](https://learn.microsoft.com/en-us/dotnet/api/system.guid.newguid?view=net-9.0#remarks) that the underlying RNG is not guaranteed to be cryptographically secure, rendering them unsuitable for security-sensitive unique keys.
633
651
6.`AsByteSpan`: A zero-allocation performance optimization unique to `ByteAether.Ulid`, exposing a direct `ReadOnlySpan<byte>` slice of the underlying structure.
Copy file name to clipboardExpand all lines: src/Dapper/PACKAGE.md
+8-4Lines changed: 8 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -31,6 +31,10 @@ Install the stable package via NuGet:
31
31
dotnet add package ByteAether.Ulid.Dapper
32
32
```
33
33
34
+
> [!NOTE]
35
+
> This package automatically includes `ByteAether.Ulid` as a transitive dependency, so installing it separately is unnecessary.
36
+
> If you do install `ByteAether.Ulid` directly, its version must be **greater than or equal to**`ByteAether.Ulid.Dapper`. Referencing an older version will trigger a **NU1605 (Package Downgrade)** build error.
37
+
34
38
## 🚀 Usage
35
39
36
40
Call `DapperUlid.RegisterUlid()` during your application's startup lifecycle (e.g., inside `Program.cs` or a global initialization block) before executing any queries:
@@ -70,18 +74,18 @@ Unlike full object-relational mappers (like Entity Framework Core) which preserv
All storage formats are technically supported, but their ability to maintain chronological sorting and support range queries depends entirely on how the underlying database provider handles GUID byte layouts. Because ULIDs rely on a big-endian timestamp for sorting, your choice of database provider determines which formats remain index-friendly:
77
+
While all storage formats are fully supported, their ability to preserve chronological order and execute valid range queries depends on how the underlying database engine handles byte-order comparisons and GUID representations. Because ULIDs rely on a big-endian timestamp for sorting, your choice of database provider determines which formats remain index-friendly:
74
78
75
79
***Globally Safe (`String` and `Binary`)**: These formats preserve the raw left-to-right chronological order of ULIDs natively across all database engines (SQLite, PostgreSQL, SQL Server, etc.).
76
80
***Provider Dependent (`Guid`)**: Standard `.NET Guid` structures use a mixed-endian layout.
77
81
* **PostgreSQL**: Supported. The connection driver automatically corrects the endianness when mapping to native `uuid` columns, preserving chronological sorting.
78
-
* **SQLite / Others**: Incompatible for range queries. These engines store GUIDs as raw byte streams, meaning the mixed-endian layout will scramble chronological comparison (though **equality operations remain fully functional**).
82
+
* **SQLite / Others**: Incompatible for range queries. These engines store GUIDs as raw bytes or text strings, causing standard mixed-endian byte ordering to corrupt chronological comparisons (**though equality lookups remain fully functional**).
79
83
***SQL Server Specific (`SqlServerGuid`)**: This format explicitly optimizes byte shuffling for Microsoft SQL Server's unique sequential indexing rules.
80
84
* **Constraint**: This format **only** works as intended if the underlying column is typed as `uniqueidentifier`. Storing it as `BINARY(16)` or `VARCHAR` will break sorting.
81
-
* **Trade-off**: This internal byte reordering sacrifices cross-database compatibility (e.g., migrating data to PostgreSQL or SQLite) in exchange for raw SQL Server index performance.
85
+
* **Trade-off**: This byte-shuffling strategy sacrifices cross-database data portability (e.g., directly reading or migrating database rows to PostgreSQL or SQLite) to optimize index page fragmentation and B-tree insertion performance in SQL Server.
82
86
83
87
> [!CAUTION]
84
-
> Before using `Guid` or `SqlServerGuid` formats for range queries (`>=`, `<=`) or `OrderBy` clauses, verify your database provider's native UUID comparison behavior. Misaligning the format with the engine's sorting behavior will result in broken data retrieval and missed records.
88
+
> Before using `Guid` or `SqlServerGuid` formats for range queries (`>=`, `<=`) or `OrderBy` clauses, verify your database provider's native UUID comparison behavior. Misaligning the format with the engine's native comparison logic will lead to incorrect query ordering and omitted records during range filtering.
0 commit comments