UUID in C#
Guid.NewGuid() returns a version 4 UUID and needs no package; since .NET 9, Guid.CreateVersion7() returns a time-ordered v7. The trap unique to .NET is that a byte round-trip through new Guid(byte[]) can corrupt a value without throwing.
By the Withuse team · Updated
Every snippet and every output below was executed on .NET SDK 10.0.400 (runtime 10.0.11) on macOS arm64.
Generating v4 and v7
Guid v4 = Guid.NewGuid(); // random Guid v7 = Guid.CreateVersion7(); // time-ordered, .NET 9+ Console.WriteLine(v4); Console.WriteLine(v7);
afa136d8-7ff6-4cf6-ad45-79b90e6a454e 01a052d1-50c2-7c2f-a741-201ae5d71af7
Note the version digit at the start of the third group: 4 in the first value, 7 in the second. Across 2,000 NewGuid() calls we saw only version digit 4 and variant nibbles 8, 9, a and b, so the output is RFC 9562 conformant with no surprises. There is also a CreateVersion7(DateTimeOffset) overload when you need to pin the embedded instant, which is useful for backfilling historical rows so their keys sort alongside new ones.
What .NET does not give you: v3 and v5
Reflecting over the Guid type on .NET 10 turns up exactly one version factory — CreateVersion7. There is no CreateVersion5, no CreateVersion3, and no namespace-based API of any kind. This is a real gap relative to Python, Java, Go and JavaScript, all of which ship name-based generation in their standard library or default package. If you need a deterministic identifier where the same input must always yield the same UUID, you have to compute the digest and set the version and variant bits yourself, or take a NuGet dependency. Do not reach for NewGuid() and a dictionary as a substitute; the whole point of v5 is that two services derive the identical value without sharing state.
.NET 9 did add Guid.AllBitsSet, which is the Max UUID ffffffff-ffff-ffff-ffff-ffffffffffff introduced by RFC 9562 as a sentinel at the opposite end of the range from Guid.Empty.
The byte round-trip that corrupts silently
This is the failure mode worth internalising, because nothing about it looks wrong at runtime:
Guid id = Guid.Parse("550e8400-e29b-41d4-a716-446655440000");
Convert.ToHexString(id.ToByteArray()) // .NET order
Convert.ToHexString(id.ToByteArray(bigEndian: true)) // RFC order
new Guid(id.ToByteArray(bigEndian: true)) // RFC bytes, default ctor00840e559be2d441a716446655440000 550e8400e29b41d4a716446655440000 00840e55-9be2-d441-a716-446655440000 <-- silently wrong
The third line took correct RFC-ordered bytes, fed them to the default constructor, and produced a different identifier. No exception, no warning, and the result is a perfectly well-formed GUID that will happily be stored, indexed and compared — it simply is not the value you started with. Because both the input and the output look like valid GUIDs, this bug typically surfaces much later as a missing row rather than as a parsing error, which makes it expensive to trace back.
Since .NET 8 the fix is symmetrical and explicit: ToByteArray(bigEndian: true) on the way out and new Guid(span, bigEndian: true) on the way in. Apply them at every boundary where raw bytes cross into or out of .NET — binary columns, message payloads, interop with a service written in another language. Our UUID vs GUID guide covers the underlying field layout and why the two conventions diverged in the first place.
Guid.Empty is not null
Guid unset = default;
Console.WriteLine($"{unset} {unset == Guid.Empty}");
// 00000000-0000-0000-0000-000000000000 TrueGuid is a struct, so it cannot be null unless you declare it as Guid?. A field you forgot to assign does not blow up; it quietly holds the all-zeros UUID, and a null check has nothing to find. This mirrors the zero-value problem in Go, and the remedy is the same: test for Guid.Empty explicitly at your validation boundary, or require the identifier as a constructor parameter so it cannot be skipped.
Sorting: we measured instead of repeating the folklore
A widely repeated claim is that .NET compares Guid values in a way that diverges from plain byte order, which would undermine the point of using v7. On .NET 10 we could not reproduce it. Ordering 1,000 random GUIDs with OrderBy(g => g) produced exactly the same sequence as ordering by RFC-ordered bytes, and v7 values generated across a full year stayed in creation order — including across the point where the leading four bytes pass 0x80000000, which means the comparison treats that field as unsigned rather than signed.
So in-process sorting of v7 GUIDs behaves the way you would hope. What this does not tell you is how your database orders the column, which depends on the storage type and the engine's own rules — a separate question from how .NET compares two values in memory. If you are choosing between random and time-ordered keys, our comparison of v4 and v7 lays out the index behaviour, and UUID in MySQL shows how the same choice plays out in a clustered index.
Frequently asked questions
How do I generate a UUID in C#?
Call Guid.NewGuid(), which is built into the base class library and needs no package. It returns a version 4 UUID — 122 random bits — and across 2,000 generated values we observed only version digit 4 and variant nibbles 8, 9, a and b, so the output is RFC 9562 conformant. .NET calls the type Guid rather than Uuid, but the value is the same 128-bit identifier under a different name. Since .NET 9 you also have Guid.CreateVersion7() for time-ordered identifiers, which is the better default for database keys. There is no configuration to choose a version on NewGuid itself; the two methods are separate entry points and each always produces its own version.
Does C# support UUID v7?
Yes, from .NET 9 onward, through Guid.CreateVersion7() and an overload taking a DateTimeOffset when you need a specific instant. We confirmed the round trip on .NET 10: Guid.CreateVersion7(DateTimeOffset.Parse("2024-03-15T09:26:40Z")) produced 018e416f-5880-78c7-a892-8ec135b7580b, whose leading 48 bits decode back to exactly that timestamp. On .NET 8 and earlier there is no built-in option, so you need a NuGet package such as UUIDNext or a hand-rolled generator that writes the millisecond counter into the first six bytes and sets the version and variant nibbles. Note that the reverse does not exist: reflecting over the type shows CreateVersion7 is the only version factory .NET provides, so v3 and v5 remain your own problem. Because the embedded timestamp is recoverable by anyone holding the value, avoid v7 for identifiers exposed in public URLs where creation times are sensitive.
Why don't my GUID bytes match the other system's?
Because Guid.ToByteArray() serialises the first three fields little-endian while RFC 9562 specifies big-endian, so the same identifier produces two different byte sequences. The dangerous direction is reading: passing RFC-ordered bytes to new Guid(byte[]) throws no exception and returns a valid-looking but wrong value. We measured it — 550e8400-e29b-41d4-a716-446655440000 came back as 00840e55-9be2-d441-a716-446655440000. Because both the input and the output are well-formed GUIDs, the mistake usually surfaces much later as a row that cannot be found rather than as a parse error. Since .NET 8 both sides take an explicit flag: ToByteArray(bigEndian: true) and new Guid(span, bigEndian: true). Apply them at every boundary where raw bytes cross into or out of .NET — binary database columns, message payloads, and interop with services written in other languages. Our UUID versus GUID guide explains the underlying field layout and why the two conventions diverged.
Is Guid.Empty the same as null?
No. Guid is a struct, so it can never be null unless you declare it as Guid?, and an unassigned Guid field or property silently takes the all-zeros value instead. We confirmed that Guid.Empty == default(Guid) is true and that a freshly constructed object's Guid property already equals 00000000-0000-0000-0000-000000000000. This is the C# equivalent of Go's uuid.Nil problem: a forgotten assignment produces a well-formed identifier rather than an error, and a null check cannot catch it because there is nothing null to find. Guard explicitly with if (id == Guid.Empty) at your validation boundary, and prefer a required constructor parameter so the value can never be skipped. Note that .NET 9 added Guid.AllBitsSet at the opposite end of the range, the Max UUID of all ones, which is likewise a legitimate value rather than an error signal.
Can I sort GUIDs chronologically in C#?
Yes, if they are version 7, and plain OrderBy(g => g) is enough. Folklore says .NET compares Guid fields in a way that diverges from byte order, so we measured it on .NET 10 rather than repeating the claim. Across 1,000 random GUIDs, OrderBy(g => g) produced exactly the same sequence as ordering by RFC-ordered bytes, and v7 values stayed in creation order even across the point where the leading four bytes pass 0x80000000, meaning the comparison treats them as unsigned. Note that this covers sorting inside your application only. A database applies its own collation to whatever column type stores the value, and that ordering is a separate question.
References
Need UUIDs without writing code? Generate v1, v3, v4, v5 and v7 in bulk with our free UUID generator, or paste an existing value into the UUID decoder to read its version and creation time.