Description
Guid.CreateVersion7() explicitly states that it "Creates a new Guid according to RFC 9562, following the Version 7 format." (RFC link).
The RFC states that the v7-UUID uses a Unix millisecond-timestamp in the most significant 48 bits (i.e. first 6 bytes in network-order).
Quoting from the RFC:
unix_ts_ms:
48-bit big-endian unsigned number of the Unix Epoch timestamp in milliseconds as per Section 6.1. Occupies bits 0 through 47 (octets 0-5).
The current .NET implementation CreateVersion7() does the following:
- Gets UtcNow timestamp. Ex.
1737048697655 as an integer, or 37-AB-2B-70-94-01-00-00 as little-endian hex bytes.
- Ignores the 2 right-most bytes (which makes sense).
- Writes a little-endian 4-byte int32 as
2B-70-94-01, then writes a little-endian int16 as 37-AB.
- The final v7 generated guid is
2B-70-94-01-37-AB-F0-75-AD-26-BD-59-2A-6E-DB-FE (but lets focus on the left-most 6 bytes of timestamp).
It seems to me that according to the RFC the correct v7 timestamp encoding should be big-endian, and thus the left-most 6 bytes should be:
01-94-70-2B-AB-37.
The .NET string representation of that generated v7 guid is "0194702b-ab37-75f0-ad26-bd592a6edbfe". Ie. the big-endian timestamp bytes appear in the string representation, but not in the byte representation.
Since .NET guid string conversion is the same for all guids (ie. uses the same logic to create a string from guid's' internal .NET field-structure), the .NET implementation must choose to either be big-endian-timestamp compliant in v7-guid's byte-order (which IMHO is what the RFC actually specifies), or be big-endian-timestamp compliant in the .NET's string representation -- but it cannot do both. The current implementation has chosen the string-order approach, which, IMHO, is not what the RFC requires.
Consequences of the current (IMHO incorrect) v7-guid implementation:
- I've seen community guidance to use
CreateVersion7() guids for PostgreSQL UUIDs (primary keys and indexed columns) to avoid uuid-fragmentation in PostgreSQL. This is wrong, because with the current CreateVersion7() implementation the 1st byte of that guid increments ~once per minute, and will rotate through 256 values after ~4+ hours (PostgreSQL compares uuids as byte-arrays, left-to-right).
Reproduction Steps
Sharplab.io reproduction
using System;
using System.Runtime.CompilerServices;
Span<byte> bytes = stackalloc byte[8];
var timestamp = DateTimeOffset.UtcNow;
long timestamp_millisec = timestamp.ToUnixTimeMilliseconds();
timestamp_millisec.Dump();
Unsafe.WriteUnaligned(ref bytes[0], timestamp_millisec);
BitConverter.ToString(bytes.ToArray()).Dump();
var guid_v7 = Guid.CreateVersion7(timestamp);
BitConverter.ToString(guid_v7.ToByteArray()).Dump();
guid_v7.ToString().Dump();
Expected behavior
v7 guid bytes should start with big-endian 6-byte Unix timestamp. See description for an example of expected behavior.
Actual behavior
v7 guid bytes currently do not start with a big-endian 6-byte Unix timestamp, as prescribed by RFC. See description for an example of actual behavior.
Regression?
No response
Known Workarounds
No response
Configuration
.NET 9.0.1 on x64 Windows 10 (Intel).
Other information
No response
Description
Guid.CreateVersion7()explicitly states that it "Creates a new Guid according to RFC 9562, following the Version 7 format." (RFC link).The RFC states that the v7-UUID uses a Unix millisecond-timestamp in the most significant 48 bits (i.e. first 6 bytes in network-order).
Quoting from the RFC:
The current .NET implementation
CreateVersion7()does the following:1737048697655as an integer, or37-AB-2B-70-94-01-00-00as little-endian hex bytes.2B-70-94-01, then writes a little-endian int16 as37-AB.2B-70-94-01-37-AB-F0-75-AD-26-BD-59-2A-6E-DB-FE(but lets focus on the left-most 6 bytes of timestamp).It seems to me that according to the RFC the correct v7 timestamp encoding should be big-endian, and thus the left-most 6 bytes should be:
01-94-70-2B-AB-37.The .NET string representation of that generated v7 guid is
"0194702b-ab37-75f0-ad26-bd592a6edbfe". Ie. the big-endian timestamp bytes appear in the string representation, but not in the byte representation.Since .NET guid string conversion is the same for all guids (ie. uses the same logic to create a string from guid's' internal .NET field-structure), the .NET implementation must choose to either be big-endian-timestamp compliant in v7-guid's byte-order (which IMHO is what the RFC actually specifies), or be big-endian-timestamp compliant in the .NET's string representation -- but it cannot do both. The current implementation has chosen the string-order approach, which, IMHO, is not what the RFC requires.
Consequences of the current (IMHO incorrect) v7-guid implementation:
CreateVersion7()guids for PostgreSQL UUIDs (primary keys and indexed columns) to avoid uuid-fragmentation in PostgreSQL. This is wrong, because with the currentCreateVersion7()implementation the 1st byte of that guid increments ~once per minute, and will rotate through 256 values after ~4+ hours (PostgreSQL compares uuids as byte-arrays, left-to-right).Reproduction Steps
Sharplab.io reproduction
Expected behavior
v7 guid bytes should start with big-endian 6-byte Unix timestamp. See description for an example of expected behavior.
Actual behavior
v7 guid bytes currently do not start with a big-endian 6-byte Unix timestamp, as prescribed by RFC. See description for an example of actual behavior.
Regression?
No response
Known Workarounds
No response
Configuration
.NET 9.0.1 on x64 Windows 10 (Intel).
Other information
No response