Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ This implementation addresses a potential `OverflowException` that can occur whe

This library uniquely addresses the predictability of monotonic ULIDs generated within the same millisecond by allowing random increments to the random component. This mitigates enumeration attack vulnerabilities, as discussed in [ULID specification issue #105](https://github.com/ulid/spec/issues/105). You can configure the random increment with a random value ranging from 1-byte (1–256) to 4-bytes (1–4,294,967,296), enhancing randomness while preserving lexicographical sortability.

For most modern systems, ULIDs offer a superior alternative to both GUIDs and integer IDs. While GUIDs provide uniqueness, they lack sortability and readability, impacting indexing and querying efficiency. Integer IDs are sortable but not universally unique, leading to potential conflicts in distributed environments. ULIDs combine universal uniqueness with lexicographical sortability, making them the optimal choice for scalable and efficient identifier generation in modern applications. This library provides a robust, reliable, and compliant ULID implementation, enabling your application to leverage these benefits without compromising performance or adherence to the official specification.
---

In the evolution of distributed identifiers, ULIDs represent the definitive successor to both legacy GUIDs and auto-incrementing integers. While modern standards like UUIDv7 attempt to address sortability, the [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#name-monotonicity-and-counters) makes monotonicity optional, allowing implementations ([such as the native .NET provider](https://github.com/dotnet/runtime/blob/571b044582ceb7fe426b7f143c703064aa9ea4db/src/libraries/System.Private.CoreLib/src/System/Guid.cs#L306)) to sacrifice strict ordering during sub-millisecond bursts. This _"lazy"_ approach reintroduces the very index fragmentation and out-of-order writes that sortable IDs were meant to solve.

ULID addresses this by design, mandating strict lexicographical sortability and monotonic increments. By enforcing these requirements at the specification level rather than leaving them to the implementor's discretion, ULID ensures consistent, high-performance behavior across all environments. This library provides a robust, compliant implementation that guarantees this order, enabling your application to scale without the performance trade-offs of non-deterministic identifiers.

## Features

Expand Down
15 changes: 15 additions & 0 deletions src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ public void New_WithTimestampAndRandom_ShouldGenerateCorrectTimestampAndRandom()
Assert.Equal(random, ulid.Random.ToArray());
}

[Fact]
public void New_WithUnixTimestampMillisecondsAndRandom_ShouldGenerateCorrectTimestampAndRandom()
{
// Arrange
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var random = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Act
var ulid = Ulid.New(timestamp, random);

// Assert
Assert.Equal(timestamp, ulid.Time.ToUnixTimeMilliseconds());
Assert.Equal(random, ulid.Random.ToArray());
}

[Fact]
public void New_NonMonotonic_CanProduceSmallerUlids()
{
Expand Down
2 changes: 1 addition & 1 deletion src/ByteAether.Ulid/Ulid.Boundaries.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace ByteAether.Ulid;

public partial struct Ulid
public readonly partial struct Ulid
{
private static readonly byte[] _randomMin = Enumerable.Repeat((byte)0x00, _ulidSizeRandom).ToArray();
private static readonly byte[] _randomMax = Enumerable.Repeat((byte)0xFF, _ulidSizeRandom).ToArray();
Expand Down
4 changes: 2 additions & 2 deletions src/ByteAether.Ulid/Ulid.Comparable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ namespace ByteAether.Ulid;
#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
#endif
public readonly int CompareTo(object? obj)
public int CompareTo(object? obj)
{
if (obj == null)
{
Expand All @@ -74,7 +74,7 @@ public readonly int CompareTo(object? obj)
#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
#endif
public readonly int CompareTo(Ulid other)
public int CompareTo(Ulid other)
=> CompareToCore(this, other);

#if NETCOREAPP3_0_OR_GREATER
Expand Down
4 changes: 2 additions & 2 deletions src/ByteAether.Ulid/Ulid.Equatable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace ByteAether.Ulid;
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public override readonly int GetHashCode()
public override int GetHashCode()
{
ref var rA = ref Unsafe.As<Ulid, int>(ref Unsafe.AsRef(in this));
return rA ^ Unsafe.Add(ref rA, 1) ^ Unsafe.Add(ref rA, 2) ^ Unsafe.Add(ref rA, 3);
Expand All @@ -28,7 +28,7 @@ public override readonly int GetHashCode()
#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
#endif
public readonly bool Equals(Ulid other)
public bool Equals(Ulid other)
=> EqualsCore(this, other);

/// <inheritdoc/>
Expand Down
2 changes: 1 addition & 1 deletion src/ByteAether.Ulid/Ulid.Guid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public static Ulid New(Guid guid)
#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
#endif
public readonly Guid ToGuid()
public Guid ToGuid()
{
#if NETCOREAPP
if (BitConverter.IsLittleEndian && _isVector128Supported)
Expand Down
48 changes: 24 additions & 24 deletions src/ByteAether.Ulid/Ulid.New.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,30 @@ ref random.GetPinnableReference(),

private static int _lastUlidLock;

#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
private static void AcquireSpinLock()
{
// Hot-path
if (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 0)
{
return;
}

// Spin until the lock is acquired
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 1)
{
spinner.SpinOnce();
}
}

#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
Expand Down Expand Up @@ -348,28 +372,4 @@ private static void IncrementByByteSpan(ref byte targetRef, ReadOnlySpan<byte> s

throw new OverflowException("Addition resulted in a value larger than the target span's capacity.");
}

#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
private static void AcquireSpinLock()
{
// Hot-path
if (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 0)
{
return;
}

// Spin until the lock is acquired
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 1)
{
spinner.SpinOnce();
}
}
}
43 changes: 19 additions & 24 deletions src/ByteAether.Ulid/Ulid.String.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public readonly partial struct Ulid
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public readonly string ToString(string? format, IFormatProvider? formatProvider) => ToString();
public string ToString(string? format, IFormatProvider? formatProvider) => ToString();

/// <summary>
/// Returns a string representation of the current instance of <see cref="Ulid"/> in its canonical Crockford's Base32 format.'
Expand All @@ -93,13 +93,13 @@ public readonly partial struct Ulid
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public override readonly string ToString()
public override string ToString()
{
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
return string.Create(UlidStringLength, this, (span, ulid) => ulid.TryFill(span, _base32Chars));
return string.Create(UlidStringLength, this, static (span, ulid) => ulid.Fill(span, _base32Chars));
#else
Span<char> span = stackalloc char[UlidStringLength];
TryFill(span, _base32Chars);
Fill(span, _base32Chars);
return span.ToString();
#endif
}
Expand Down Expand Up @@ -292,21 +292,22 @@ public static bool TryParse(ReadOnlySpan<byte> s, IFormatProvider? provider, out
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public readonly bool TryFormat(
public bool TryFormat(
Span<char> destination,
out int charsWritten,
ReadOnlySpan<char> format,
IFormatProvider? provider = null
)
{
if (TryFill(destination, _base32Chars))
if (destination.Length < UlidStringLength)
{
charsWritten = UlidStringLength;
return true;
charsWritten = 0;
return false;
}

charsWritten = 0;
return false;
Fill(destination, _base32Chars);
charsWritten = UlidStringLength;
return true;
}

/// <summary>
Expand All @@ -324,33 +325,29 @@ public readonly bool TryFormat(
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public readonly bool TryFormat(
public bool TryFormat(
Span<byte> destination,
out int bytesWritten,
ReadOnlySpan<char> format,
IFormatProvider? provider = null
)
{
if (TryFill(destination, _base32Bytes))
if (destination.Length < UlidStringLength)
{
bytesWritten = UlidStringLength;
return true;
bytesWritten = 0;
return false;
}

bytesWritten = 0;
return false;
Fill(destination, _base32Bytes);
bytesWritten = UlidStringLength;
return true;
}

#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
#endif
private bool TryFill<T>(Span<T> span, T[] map)
private void Fill<T>(Span<T> span, T[] map)
{
if (span.Length < UlidStringLength)
{
return false;
}

// Encode randomness
span[25] = map[_r9 & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][111|11111|]
span[24] = map[((_r8 & 0x3) << 3) | (_r9 >> 5)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][111111|11][111|11111]
Expand Down Expand Up @@ -380,8 +377,6 @@ private bool TryFill<T>(Span<T> span, T[] map)
span[2] = map[_t1 >> 3]; // 00[11111111][|11111|111][11111111][11111111][11111111][11111111]
span[1] = map[_t0 & 0x1F]; // 00[111|11111|][11111111][11111111][11111111][11111111][11111111]
span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111]

return true;
}

/// <summary>
Expand Down