Last active
July 24, 2026 13:57
-
-
Save peterhirn/41d59ac3c7629084e471a720a3370e25 to your computer and use it in GitHub Desktop.
Minimal UUIDv7 C# implementation (.NET Core and .NET Framework)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Ported from https://github.com/uuidjs/uuid (MIT) | |
| using System.Buffers.Binary; | |
| using System.Runtime.CompilerServices; | |
| using System.Security.Cryptography; | |
| public static class Uuid7 | |
| { | |
| const byte Version = 0x70; | |
| private static ulong Previous = 0; | |
| private static uint Seq = 0; | |
| #if NET9_0_OR_GREATER | |
| private static readonly Lock Lock = new(); | |
| #else | |
| private static readonly object Lock = new(); | |
| #endif | |
| public static readonly DateTime Epoch = new(1970, 01, 01, 00, 00, 00, DateTimeKind.Utc); | |
| #if !NET | |
| private static readonly RandomNumberGenerator Generator = RandomNumberGenerator.Create(); | |
| #endif | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static ulong Now() => (ulong)(DateTime.UtcNow - Epoch).TotalMilliseconds; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static void FixByteOrder(Span<byte> uuid) | |
| { | |
| if (BitConverter.IsLittleEndian) | |
| { | |
| uuid.Slice(0, 4).Reverse(); | |
| uuid.Slice(4, 2).Reverse(); | |
| uuid.Slice(6, 2).Reverse(); | |
| } | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static (ulong Now, uint Seq) UpdateState(ulong now, Span<byte> random) | |
| { | |
| lock (Lock) | |
| { | |
| if (now > Previous) | |
| { | |
| // Time has moved on! Pick a new random sequence number | |
| Seq = BinaryPrimitives.ReadUInt32BigEndian(random.Slice(6)); | |
| Previous = now; | |
| } | |
| else | |
| { | |
| // Bump sequence counter w/ 32-bit rollover | |
| Seq++; | |
| // In case of rollover, bump timestamp to preserve monotonicity. This is | |
| // allowed by the RFC and should self-correct as the system clock catches | |
| // up. See https://www.rfc-editor.org/rfc/rfc9562.html#section-6.2-9.4 | |
| if (Seq == 0) | |
| { | |
| Previous++; | |
| } | |
| } | |
| return (Previous, Seq); | |
| } | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public static Guid Create(ulong now, uint seq, Span<byte> random) | |
| { | |
| if (random.Length < 16) | |
| throw new ArgumentException("Random bytes length must be >= 16", nameof(random)); | |
| Span<byte> buffer = stackalloc byte[18]; | |
| var uuid = buffer.Slice(2); | |
| // byte 0-5: timestamp (48 bits) | |
| BinaryPrimitives.WriteUInt64BigEndian(buffer, now); | |
| // byte 6: `version` (4 bits) | sequence bits 28-31 (4 bits) | |
| uuid[6] = (byte)(Version | ((seq >>> 28) & 0x0f)); | |
| // byte 7: sequence bits 20-27 (8 bits) | |
| uuid[7] = (byte)((seq >>> 20) & 0xff); | |
| // byte 8: `variant` (2 bits) | sequence bits 14-19 (6 bits) | |
| uuid[8] = (byte)(0x80 | ((seq >>> 14) & 0x3f)); | |
| // byte 9: sequence bits 6-13 (8 bits) | |
| uuid[9] = (byte)((seq >>> 6) & 0xff); | |
| // byte 10: sequence bits 0-5 (6 bits) | random (2 bits) | |
| uuid[10] = (byte)(((seq << 2) & 0xff) | (byte)(random[10] & 0x03)); | |
| // bytes 11-15: random (40 bits) | |
| random.Slice(11, 5).CopyTo(uuid.Slice(11)); | |
| #if NET | |
| return new(uuid, true); | |
| #else | |
| FixByteOrder(uuid); | |
| return new(uuid.ToArray()); | |
| #endif | |
| } | |
| public static Guid Create() | |
| { | |
| #if NET | |
| Span<byte> random = stackalloc byte[16]; | |
| RandomNumberGenerator.Fill(random); | |
| #else | |
| byte[] random = new byte[16]; | |
| Generator.GetBytes(random); | |
| #endif | |
| var (now, seq) = UpdateState(Now(), random); | |
| return Create(now, seq, random); | |
| } | |
| public static DateTime Timestamp(Guid input) | |
| { | |
| Span<byte> buffer = stackalloc byte[18]; | |
| var uuid = buffer.Slice(2); | |
| #if NET | |
| input.TryWriteBytes(uuid, true, out _); | |
| #else | |
| MemoryExtensions.CopyTo(input.ToByteArray(), uuid); | |
| FixByteOrder(uuid); | |
| #endif | |
| var version = (byte)(uuid[6] & 0xf0); | |
| if (version != Version) | |
| throw new ArgumentException($"Invalid UUID version ({version >> 4:x1})"); | |
| var timestamp = BinaryPrimitives.ReadUInt64BigEndian(buffer); | |
| return Epoch + TimeSpan.FromMilliseconds(timestamp); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Collections.Concurrent; | |
| using System.Globalization; | |
| using NUnit.Framework; | |
| [TestFixture] | |
| public class Uuid7Tests | |
| { | |
| // Fixture values for testing with the rfc v7 UUID example: | |
| // https://www.rfc-editor.org/rfc/rfc9562.html#name-example-of-a-uuidv7-value | |
| static readonly Guid RFC_V7 = new("017f22e2-79b0-7cc3-98c4-dc0c0c07398f"); | |
| const ulong RFC_NOW = 0x17f22e279b0; | |
| const uint RFC_SEQ = ((uint)0x0cc3 << 20) | (0x98c4dc >> 2); | |
| static readonly byte[] RFC_RANDOM = [ | |
| 0x10, | |
| 0x91, | |
| 0x56, | |
| 0xbe, | |
| 0xc4, | |
| 0xfb, | |
| 0x0c, | |
| 0xc3, | |
| 0x18, | |
| 0xc4, | |
| 0x6c, | |
| 0x0c, | |
| 0x0c, | |
| 0x07, | |
| 0x39, | |
| 0x8f, | |
| ]; | |
| [Test] | |
| public void Subsequent_Uuids_Are_Different() | |
| { | |
| var first = Uuid7.Create(); | |
| var second = Uuid7.Create(); | |
| Assert.That(first, Is.Not.EqualTo(second)); | |
| } | |
| [Test] | |
| public void Subsequent_Uuids_Are_Increasing() | |
| { | |
| var first = Uuid7.Create(); | |
| var second = Uuid7.Create(); | |
| Assert.That(second, Is.GreaterThan(first)); | |
| } | |
| [Test] | |
| public void Uuid_v7_RFC() | |
| { | |
| var rfc = Uuid7.Create(RFC_NOW, RFC_SEQ, RFC_RANDOM); | |
| Assert.That(rfc, Is.EqualTo(RFC_V7)); | |
| } | |
| [Test] | |
| public void Concurrently_Created_Uuids_Are_Increasing() | |
| { | |
| var taskCount = 1_000; | |
| var uuidCount = taskCount * 2; | |
| var tasks = new Task[taskCount]; | |
| var results = new ConcurrentDictionary<Guid, byte>(Environment.ProcessorCount, uuidCount); | |
| for (int i = 0; i < taskCount; i++) | |
| { | |
| tasks[i] = Task.Run(() => | |
| { | |
| var first = Uuid7.Create(); | |
| var second = Uuid7.Create(); | |
| if (!results.TryAdd(first, 0)) | |
| throw new InvalidOperationException("Duplicated UUID v7"); | |
| if (!results.TryAdd(second, 0)) | |
| throw new InvalidOperationException("Duplicated UUID v7"); | |
| Assert.That(second, Is.GreaterThan(first)); | |
| }); | |
| } | |
| Task.WaitAll(tasks); | |
| Assert.That(results, Has.Count.EqualTo(uuidCount)); | |
| } | |
| [Test] | |
| public void Timestamp_Throws_Invalid_Version() | |
| { | |
| var v1 = new Guid("16bd5b94-829e-11f1-ab07-0242ac120002"); | |
| var v4 = new Guid("a82b4747-b341-4b76-bc97-112f53b9dc1c"); | |
| var nil = new Guid("00000000-0000-0000-0000-000000000000"); | |
| var max = new Guid("ffffffff-ffff-ffff-ffff-ffffffffffff"); | |
| using (Assert.EnterMultipleScope()) | |
| { | |
| Assert.Throws(Is.TypeOf<ArgumentException>() | |
| .And.Message.EqualTo("Invalid UUID version (1)"), | |
| () => Uuid7.Timestamp(v1)); | |
| Assert.Throws(Is.TypeOf<ArgumentException>() | |
| .And.Message.EqualTo("Invalid UUID version (4)"), | |
| () => Uuid7.Timestamp(v4)); | |
| Assert.Throws(Is.TypeOf<ArgumentException>() | |
| .And.Message.EqualTo("Invalid UUID version (0)"), | |
| () => Uuid7.Timestamp(nil)); | |
| Assert.Throws(Is.TypeOf<ArgumentException>() | |
| .And.Message.EqualTo("Invalid UUID version (f)"), | |
| () => Uuid7.Timestamp(max)); | |
| } | |
| } | |
| [Test] | |
| public void Timestamp_RFC() | |
| { | |
| var expected = Uuid7.Epoch + TimeSpan.FromMilliseconds(RFC_NOW); | |
| Assert.That(Uuid7.Timestamp(RFC_V7), Is.EqualTo(expected)); | |
| } | |
| [Test] | |
| public void Timestamp_Example() | |
| { | |
| var guid = new Guid("019f74fd-d069-7776-9684-a259beccc9ac"); | |
| var actual = Uuid7.Timestamp(guid); | |
| var expected = | |
| DateTime.Parse( | |
| "2026-07-18T11:30:18.857Z", | |
| CultureInfo.InvariantCulture, | |
| DateTimeStyles.RoundtripKind); | |
| using (Assert.EnterMultipleScope()) | |
| { | |
| Assert.That(expected.Kind, Is.EqualTo(DateTimeKind.Utc)); | |
| Assert.That(actual.Kind, Is.EqualTo(DateTimeKind.Utc)); | |
| Assert.That(actual, Is.EqualTo(expected)); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment