Skip to content

Commit fc71175

Browse files
authored
Clarified README.md and PACKAGE.md documents. (#59)
1 parent 8a5ec78 commit fc71175

5 files changed

Lines changed: 71 additions & 39 deletions

File tree

README.md

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,19 @@ ULIDs (Universally Unique Lexicographically Sortable Identifiers) offer a modern
2929

3030
### Resilient Concurrency & Monotonic Overflow Handling
3131

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

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

3636
### Mitigating Enumeration Attacks
3737

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 4 bytes) 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.
3939

4040
### ULID vs UUIDv7
4141

4242
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.
4343

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

4646
**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.
4747

@@ -64,7 +64,7 @@ This library explicitly **multi-targets** each runtime version listed below, ena
6464
- **Lock-Free Synchronization**: Monotonic generation utilizes a high-performance, **lock-free compare-and-exchange (CAS)** approach.
6565
- **Specification-Compliant**: Fully adheres to the ULID specification.
6666
- **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.
6868
- **Error-Free Generation**: Prevents `OverflowException` by incrementing the timestamp component when the random part overflows, ensuring continuous unique ULID generation.
6969

7070
### Extension Packages
@@ -118,7 +118,7 @@ Console.WriteLine($"ULID: {ulid}, GUID: {guid}, String: {ulidString}");
118118
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:
119119

120120
```csharp
121-
// Define the temporal boundaries of your window
121+
// Define temporal boundaries for the target window
122122
DateTimeOffset startTime = DateTimeOffset.UtcNow.AddDays(-7);
123123
DateTimeOffset endTime = DateTimeOffset.UtcNow;
124124

@@ -137,7 +137,7 @@ var query = "SELECT * FROM Records WHERE Id >= @Min AND Id <= @Max";
137137
> [!IMPORTANT]
138138
> **Database Persistence Considerations**
139139
>
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:
141141
> * **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.
142142
> * **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.
143143
>
@@ -208,7 +208,7 @@ The `Ulid` implementation provides the following properties and methods:
208208
- `Ulid.New(ReadOnlySpan<byte> bytes)`\
209209
Creates a ULID from an existing byte array.
210210
- `Ulid.New(Guid guid)`\
211-
Create from an existing `Guid`.
211+
Creates a ULID from an existing `Guid`.
212212
- `Ulid.MinAt(DateTimeOffset datetime)`\
213213
Creates the minimum possible ULID value for the specified `DateTimeOffset`.
214214
- `Ulid.MinAt(long timestamp)`\
@@ -221,11 +221,11 @@ The `Ulid` implementation provides the following properties and methods:
221221
### Checking Validity
222222

223223
- `Ulid.IsValid(string ulidString)`\
224-
Validates if the given string is a valid ULID.
224+
Validates whether the specified string represents a valid ULID.
225225
- `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.
227227
- `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.
229229

230230
### Parsing
231231

@@ -251,7 +251,7 @@ The `Ulid` implementation provides the following properties and methods:
251251
- `.Time`\
252252
Gets the timestamp component of the ULID as a `DateTimeOffset`.
253253
- `.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>`.
255255
- `.Random`\
256256
Gets the random component of the ULID as a `ReadOnlySpan<byte>`.
257257

@@ -329,6 +329,7 @@ To seamlessly use ULIDs with [Entity Framework Core](https://github.com/dotnet/e
329329
```sh
330330
dotnet add package ByteAether.Ulid.EntityFrameworkCore
331331
```
332+
332333
Register the ULID conventions within your `DbContext` via the `ConfigureConventions` method. You can choose from various underlying storage strategies (`String`, `Binary`, `Guid`, or `SqlServerGuid`):
333334

334335
```csharp
@@ -338,7 +339,8 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura
338339
{
339340
// Registers mapping for both Ulid and Ulid? types.
340341
// Supports: UlidStorageFormat.String (Default), Binary, Guid, and SqlServerGuid
341-
configurationBuilder.RegisterUlid(UlidStorageFormat.Binary);
342+
configurationBuilder
343+
.RegisterUlid(UlidStorageFormat.Binary);
342344
}
343345
```
344346
#### Per-Property Mapping (Fine-Grained Control)
@@ -440,7 +442,7 @@ DapperUlid.RegisterUlid(UlidStorageFormat.Binary);
440442
```
441443

442444
> [!NOTE]
443-
> 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.
444446
445447
More details in the package's [PACKAGE.md](./src/Dapper/PACKAGE.md) file.
446448

@@ -450,20 +452,30 @@ To use ULIDs with **Newtonsoft.Json**, you need to create a custom **JsonConvert
450452

451453
#### 1. Create the Custom JsonConverter
452454

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:
454456
```csharp
455457
using Newtonsoft.Json;
456458
using System;
457459

458460
public class UlidJsonConverter : JsonConverter<Ulid>
459461
{
460-
public override Ulid ReadJson(JsonReader reader, Type objectType, Ulid existingValue, bool hasExistingValue, JsonSerializer serializer)
462+
public override Ulid ReadJson(
463+
JsonReader reader,
464+
Type objectType,
465+
Ulid existingValue,
466+
bool hasExistingValue,
467+
JsonSerializer serializer
468+
)
461469
{
462470
var value = (string)reader.Value;
463471
return Ulid.Parse(value);
464472
}
465473

466-
public override void WriteJson(JsonWriter writer, Ulid value, JsonSerializer serializer)
474+
public override void WriteJson(
475+
JsonWriter writer,
476+
Ulid value,
477+
JsonSerializer serializer
478+
)
467479
{
468480
writer.WriteValue(value.ToString());
469481
}
@@ -502,13 +514,19 @@ using MessagePack.Formatters;
502514

503515
public class UlidFormatter : IMessagePackFormatter<Ulid>
504516
{
505-
public Ulid Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
517+
public Ulid Deserialize(
518+
ref MessagePackReader reader,
519+
MessagePackSerializerOptions options
520+
)
506521
{
507522
var bytes = reader.ReadByteArray();
508523
return Ulid.New(bytes);
509524
}
510525

511-
public void Serialize(ref MessagePackWriter writer, Ulid value, MessagePackSerializerOptions options)
526+
public void Serialize(
527+
ref MessagePackWriter writer,
528+
Ulid value, MessagePackSerializerOptions options
529+
)
512530
{
513531
writer.Write(value.ToByteArray());
514532
}
@@ -626,9 +644,9 @@ Job=DefaultJob
626644
Alternative .NET ecosystem solutions exhibit design constraints or spec deviations under heavy production loads:
627645

628646
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.
630648
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.
632650
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.
633651
6. `AsByteSpan`: A zero-allocation performance optimization unique to `ByteAether.Ulid`, exposing a direct `ReadOnlySpan<byte>` slice of the underlying structure.
634652

src/Dapper/PACKAGE.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Install the stable package via NuGet:
3131
dotnet add package ByteAether.Ulid.Dapper
3232
```
3333

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+
3438
## 🚀 Usage
3539

3640
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
7074

7175
### 2. Range Queries & Sorting Compatibility (`>=`, `<=`, `ORDER BY`)
7276

73-
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:
7478

7579
* **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.).
7680
* **Provider Dependent (`Guid`)**: Standard `.NET Guid` structures use a mixed-endian layout.
7781
* **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**).
7983
* **SQL Server Specific (`SqlServerGuid`)**: This format explicitly optimizes byte shuffling for Microsoft SQL Server's unique sequential indexing rules.
8084
* **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.
8286

8387
> [!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.
8589
8690
## ⚡ Native AOT & Trimming Compatibility
8791

0 commit comments

Comments
 (0)