|
| 1 | +using System.Threading; |
| 2 | + |
| 3 | +namespace System; |
| 4 | + |
| 5 | +/// <summary> |
| 6 | +/// Provides helper methods for GUID generation. |
| 7 | +/// </summary> |
| 8 | +internal static class GuidHelpers |
| 9 | +{ |
| 10 | + private static long s_counter; |
| 11 | + |
| 12 | + /// <summary> |
| 13 | + /// Creates a monotonically increasing UUID v7 GUID with the specified timestamp. |
| 14 | + /// Uses a globally increasing counter for strict monotonicity. |
| 15 | + /// </summary> |
| 16 | + /// <param name="timestamp">The timestamp to embed in the GUID.</param> |
| 17 | + /// <returns>A new monotonically increasing UUID v7 GUID.</returns> |
| 18 | + /// <remarks> |
| 19 | + /// <para> |
| 20 | + /// This method cannot be replaced with <c>Guid.CreateVersion7(DateTimeOffset)</c> because |
| 21 | + /// the built-in .NET implementation uses random bits for intra-millisecond uniqueness, |
| 22 | + /// which does not guarantee strict monotonicity. For keyset pagination to work correctly, |
| 23 | + /// GUIDs created within the same millisecond must be strictly ordered by creation time. |
| 24 | + /// </para> |
| 25 | + /// <para> |
| 26 | + /// This implementation uses a globally monotonically increasing counter to ensure that |
| 27 | + /// all generated GUIDs are strictly ordered by creation time, regardless of timestamp. |
| 28 | + /// </para> |
| 29 | + /// </remarks> |
| 30 | + public static Guid CreateMonotonicUuid(DateTimeOffset timestamp) |
| 31 | + { |
| 32 | + // UUID v7 format (RFC 9562): |
| 33 | + // - 48 bits: Unix timestamp in milliseconds (big-endian) |
| 34 | + // - 4 bits: version (0111 = 7) |
| 35 | + // - 12 bits: counter/sequence (for intra-millisecond ordering) |
| 36 | + // - 2 bits: variant (10) |
| 37 | + // - 62 bits: random |
| 38 | + |
| 39 | + long timestampMs = timestamp.ToUnixTimeMilliseconds(); |
| 40 | + long counter = Interlocked.Increment(ref s_counter); |
| 41 | + |
| 42 | + // Start with a random GUID and twiddle the relevant bits |
| 43 | + Guid baseGuid = Guid.NewGuid(); |
| 44 | + |
| 45 | +#if NETSTANDARD2_0 |
| 46 | + byte[] bytes = baseGuid.ToByteArray(); |
| 47 | +#else |
| 48 | + Span<byte> bytes = stackalloc byte[16]; |
| 49 | + baseGuid.TryWriteBytes(bytes); |
| 50 | +#endif |
| 51 | + |
| 52 | + // Guid.ToByteArray() returns bytes in little-endian order for the first 3 components, |
| 53 | + // but we need big-endian for UUID v7. The byte layout from ToByteArray() is: |
| 54 | + // [0-3]: Data1 (little-endian int) |
| 55 | + // [4-5]: Data2 (little-endian short) |
| 56 | + // [6-7]: Data3 (little-endian short) |
| 57 | + // [8-15]: Data4 (byte array, unchanged) |
| 58 | + |
| 59 | + // Set timestamp (48 bits) - need to account for little-endian layout |
| 60 | + // Data1 (bytes 0-3, little-endian) contains timestamp bits 0-31 |
| 61 | + bytes[0] = (byte)(timestampMs >> 8); |
| 62 | + bytes[1] = (byte)(timestampMs >> 16); |
| 63 | + bytes[2] = (byte)(timestampMs >> 24); |
| 64 | + bytes[3] = (byte)(timestampMs >> 32); |
| 65 | + |
| 66 | + // Data2 (bytes 4-5, little-endian) contains timestamp bits 32-47 |
| 67 | + bytes[4] = (byte)timestampMs; |
| 68 | + bytes[5] = (byte)(timestampMs >> 40); |
| 69 | + |
| 70 | + // Data3 (bytes 6-7, little-endian) contains version (4 bits) + counter high (12 bits) |
| 71 | + // Version 7 = 0111, counter uses 12 bits |
| 72 | + bytes[6] = (byte)(counter & 0xFF); |
| 73 | + bytes[7] = (byte)(0x70 | ((counter >> 8) & 0x0F)); |
| 74 | + |
| 75 | + // Set variant (10) in high 2 bits of byte 8 |
| 76 | + bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80); |
| 77 | + |
| 78 | + return new Guid(bytes); |
| 79 | + } |
| 80 | +} |
0 commit comments