Skip to content

Guid.CreateVersion7() - bug? #111503

@sdrapkin

Description

@sdrapkin

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.RuntimequestionAnswer questions and provide assistance, not an issue with source code or documentation.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions