HyperUuid 0.6.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package HyperUuid --version 0.6.0
                    
NuGet\Install-Package HyperUuid -Version 0.6.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="HyperUuid" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="HyperUuid" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="HyperUuid" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add HyperUuid --version 0.6.0
                    
#r "nuget: HyperUuid, 0.6.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package HyperUuid@0.6.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=HyperUuid&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=HyperUuid&version=0.6.0
                    
Install as a Cake Tool

HyperUuid

CI NuGet License: MIT

UuidGenerator.NewV4() beats Guid.NewGuid() by ~5.7x — with zero heap allocation, on every version including v5 — because it calls straight into a native Rust core instead of the BCL's own managed generator.

RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, calling directly into the native libhyperuuid shared library via source-generated LibraryImport P/Invoke — no runtime bridge, no reflection, AOT/trim-friendly. Ships as RID-specific native assets inside the package the standard NuGet way.

using HyperUuid;

var id = UuidGenerator.NewV4();
var id2 = UuidGenerator.NewV5(UuidGenerator.Namespaces.Dns, "example.com");
var id3 = UuidGenerator.NewV6();
var id4 = UuidGenerator.NewV7();

// Time-sortable versions round-trip their embedded timestamp:
DateTimeOffset created = UuidGenerator.V7Timestamp(id4);

// Version-agnostic: null instead of assuming id4 is v6/v7:
DateTimeOffset? maybeCreated = UuidGenerator.GetTimestamp(id4);

// Same thing as an extension property on Guid itself — works on any Guid, whatever made it:
DateTimeOffset? created7 = id4.Timestamp;              // the creation time
DateTimeOffset? none = Guid.NewGuid().Timestamp;       // null — a v4 UUID carries no time

// Byte order SQL Server's uniqueidentifier needs on the wire to sort by creation order:
Guid sqlOrdered = UuidGenerator.V7ToSqlOrder(id4);

// Bulk generation shares one timestamp capture, one random-bytes fetch, and (v7) one
// contiguous counter reservation across the whole batch:
Guid[] batch = UuidGenerator.NewV7Batch(1000);

Returns plain System.Guid — this binding does no byte-order conversion of its own beyond the bigEndian: true Guid constructor overload (.NET 8+), since that's already the correct, direct RFC 9562 mapping. UuidGenerator.Namespaces.Dns/Url/Oid/X500 are RFC 9562 §6.6's well-known namespaces; UuidGenerator.Nil/Max are the §5.9/§5.10 special values (Nil is literally Guid.Empty). NewV6/NewV7 also accept a DateTimeOffset directly (NewV6(DateTimeOffset)), not just a raw millisecond count; GetTimestamp is the version-agnostic counterpart to V6Timestamp/V7Timestamp — it checks the version nibble itself and returns null for anything but a genuine v6/v7 Guid, instead of assuming the caller already knows.

Guid.Timestamp is that same call spelled as an extension property, via a C# 14 extension block (GuidExtensions). It exists because the classic this Guid extension form can only express methods, and a timestamp recovered from bits the value already holds is a projection, not an action — so it wants to read as a property, the way every comparable accessor on a .NET value type does. It is a re-spelling, not a second implementation: GetTimestamp holds the logic and a test pins the two to identical results across every version, so they cannot drift. Nothing about it is package-specific either — it reads a Guid.CreateVersion7() value from the BCL just as happily, since both write the same RFC 9562 layout. Extension members lower to ordinary static calls, so this adds no allocation and nothing for the trimmer or Native AOT to chase.

Before the first UUID, UuidGenerator.IsAvailable says whether the native library resolved and UuidGenerator.NativeVersion names the core it loaded — the probe a consumer with a managed fallback (Guid.NewGuid(), Guid.CreateVersion7()) gates on, instead of catching DllNotFoundException around its first real call. It is probed once and never throws; every other member lets a load failure propagate, the Try* forms included, which report the native layer's own return codes and nothing else. A library that loaded but predates the probe (0.3.0 and earlier) reads as unavailable too: a stale binary beside a newer binding is exactly the mismatch it exists to name.

Why not Guid.NewGuid() / Guid.CreateVersion7()?

  1. It's measurably faster, not just different. Real BenchmarkDotNet numbers, [MemoryDiagnoser], linux-x64 on an Intel Core i9-11900H (dotnet run -c Release --project csharp/HyperUuid.Benchmarks -- --filter *Generation*, from the repo root):

    Method Mean Allocated
    Guid.NewGuid() 316.05 ns 0 B
    UuidGenerator.NewV4() 55.33 ns (5.71x faster) 0 B
    UuidGenerator.NewV5() 87.22 ns (3.62x faster) 0 B
    UuidGenerator.NewV6() 39.07 ns (8.09x faster) 0 B
    UuidGenerator.NewV7() 42.21 ns (7.49x faster) 0 B

    Including NewV5(Guid, string) — it used to allocate 40 B encoding the name to UTF-8 via Encoding.UTF8.GetBytes(name); now it UTF-8-encodes into a 256-byte stack buffer with an ArrayPool fallback for longer names, the same technique the batch methods already used (and, before that, proven in this project's own SequentialGuid library). NewV5(Guid, ReadOnlySpan<char>) is the same path for a name you hold as a slice rather than a string — one field of a parsed line, a pooled buffer — so nothing has to be materialized first, and NewV5(Guid, ReadOnlySpan<byte>) skips the encode step entirely: it hashes the bytes exactly as given, which also makes it the overload for a name that is not text at all.

  2. A real monotonic counter. Guid.CreateVersion7() implements no counter (RFC 9562 §6.2 Method 1): two BCL v7 GUIDs minted in the same millisecond sort randomly relative to each other, which is exactly the clustered-index fragmentation problem v7 adoption exists to solve. UuidGenerator.NewV7() reserves a slot in a process-global counter every call, guaranteeing strict creation order under concurrency — verified across interleaved individual and batch calls in this project's own test suite, not just in isolation.

  3. v6, which the BCL doesn't have at all. A field-compatible reordering of v1 for the same sort/index locality as v7, useful when you're migrating off legacy v1 IDs. No Guid.CreateVersion6 exists anywhere in the BCL.

  4. Batch generation, in three shapes. One native call, one random-bytes fetch, one counter reservation for the whole batch, instead of paying per-item overhead a thousand times. Guid.NewGuid()/CreateVersion7() have no bulk API; you'd write that loop yourself. Same machine and methodology as the table above (--filter *Batch*, 1000 UUIDs per operation):

    Method Mean vs. individual Allocated
    NewV7() x1000 individually 41.01 µs 1.00x 0 B
    NewV7Batch(1000) → new Guid[] 11.84 µs 3.46x faster 16,024 B
    FillV7(Span<Guid>) into an existing array 11.13 µs 3.68x faster 0 B
    FillV7(Span<byte>) into an existing buffer 9.61 µs 4.27x faster 0 B

    The three rows amortize different things, which is why all three exist. NewV7Batch amortizes the FFI call but still allocates the result array. FillV7(Span<Guid>) drops the allocation entirely but still pays a new Guid(chunk, bigEndian: true) conversion per element, because Guid's in-memory layout is mixed-endian and isn't RFC byte order. FillV7(Span<byte>) drops that conversion too — the native core already writes RFC-ordered bytes contiguously into your buffer — and that 1.5 µs gap between the last two rows is the per-element conversion cost, measured. FillV6/NewV6Batch behave the same way (13.98 / 13.20 / 11.70 µs respectively, against 36.83 µs for a thousand individual NewV6() calls).

  5. Cross-language consistency. The exact same Rust core also mints v5 namespace UUIDs for Ruby, Python, Go, and every other binding in this repo — verified in CI to match Python's own uuid.uuid5 byte-for-byte. If your system isn't C#-only, that's not something the BCL can offer at all.

  6. SQL Server byte ordering, for free. UuidGenerator.V7ToSqlOrder(id4) converts a version 7 UUID to the byte order System.Data.SqlTypes.SqlGuid comparison — and therefore T-SQL ORDER BY on a uniqueidentifier column — needs to sort by creation order (V6ToSqlOrder does the same for version 6), the same permutation this project's own SequentialGuid/Svartalfheim already use. Verified directly against the real SqlGuid comparator in this package's own test suite, not a hand-rolled stand-in — and it's the same native function every other binding in this repo calls, not a C#-only reimplementation. Neither Guid.NewGuid() nor Guid.CreateVersion7() has any such concept.

  7. Non-throwing and zero-conversion call shapes, for callers that need them. Every operation the native layer can fail — a random-source failure, or a timestamp its field cannot hold — has a Try twin that reports it as false rather than an exception: TryNewV4/TryNewV6/TryNewV7 for single IDs, TryFillV6/TryFillV7 for batches into a buffer you own. No exception ever crosses the P/Invoke boundary in either shape (the native layer signals with an int return code; see rust/src/ffi.rs), but the Try form lets a Result-style gateway branch on failure without wrapping every call in a try/catch. What has no twin, on purpose: NewV5 has no failure mode to report (a hash of your own bytes, no random source and no timestamp); the allocating NewV6Batch/NewV7Batch are conveniences over the fills, so the non-throwing batch is TryFill* into your own array; and V7Timestamp throws only where DateTimeOffset itself cannot represent the year. Nor does Try cover a native library that never loaded — that is UuidGenerator.IsAvailable's question, asked once up front. Separately, the SQL-order transforms and the batch fills both have raw-Span<byte> overloads that never construct a Guid at all — RFC-ordered bytes in, transformed bytes out, in place. That matters for two reasons: it's the form a byte-level correctness oracle can be pointed at directly (no need to model Guid's mixed-endian field layout to compare results), and it's measurably the fastest batch path, since the native core already writes RFC bytes contiguously into your buffer and the Guid overload has to convert every element on the way out.

The honest trade-off: this is a native dependency (a platform-specific libhyperuuid.so/.dylib/.dll bundled per-RID) instead of a BCL type that's always just there. If you only need plain v4 randomness in a C#-only codebase, Guid.NewGuid() is simpler and that's a completely reasonable choice.

AOT

Publishes cleanly under PublishAot — LibraryImport is source-generated with no runtime reflection anywhere in this assembly, and the project opts into (and fails the build on) the trim/Native-AOT analyzers via IsAotCompatible.

That claim is reproducible rather than asserted. HyperUuid.AotSmokeTest/ is a real AOT-published console app that crosses every native entry point the binding declares — the twelve uuid_* functions and hyperuuid_version — through every call shape the public surface offers: the version probe; v4, v5, v6 and v7 generation, with v5 against the RFC 9562 Appendix A.4 vector on all three name overloads and for an empty name; the non-throwing Try* path (including that an out-of-range timestamp is reported, not thrown); both SQL-order directions for both versions, raw bytes against their Guid counterparts; the batch fills into Guid and raw-byte buffers and the allocating batches; and the Guid.Timestamp extension property. It returns a nonzero exit code on any mismatch:

dotnet publish csharp/HyperUuid.AotSmokeTest/HyperUuid.AotSmokeTest.csproj \
  -c Release -r linux-x64 -p:PublishAot=true
./csharp/HyperUuid.AotSmokeTest/bin/Release/net10.0/linux-x64/publish/HyperUuid.AotSmokeTest

Last verified on linux-x64 and, inside an Alpine container, linux-musl-x64: zero ILxxxx/AOTxxxx trim or AOT diagnostics, a 1.5 MB native binary with the core inside it and no shared library beside it, and ALL NATIVE AOT CHECKS PASSED with exit code 0. TreatWarningsAsErrors is on for the library project, so an analyzer warning is a build failure, not a line in a log nobody reads. CI re-proves it per platform on every PR (see Native binary provenance).

The core is linked into the executable. A Native AOT publish does not load libhyperuuid.so (or the .dylib, or the .dll): the package carries the core as a static library for each RID under staticlibs/, and its targets file hands the one for your RID to the AOT linker and binds every P/Invoke as a direct call. The publish directory holds one executable and no native library beside it, UuidGenerator.IsAvailable is always true, and this package and HyperCast's can both be linked into the same executable. Nothing to configure; <HyperUuidStaticLink>false</HyperUuidStaticLink> in the project puts it back to loading the shared library, and so does publishing for a RID the package has no archive for. A JIT process is unaffected: it cannot link an archive, and loads the shared library as before.

WebAssembly (Blazor)

Works from a plain <PackageReference Include="HyperUuid" /> on .NET 11 and later — no <NativeFileReference>, no hand-written P/Invoke, no bridging in your own Rust build. Verified for real, in an actual headless Chromium session: HyperUuid.WasmSmokeTest (below) answers the version probe, generates v4s, matches the RFC 9562 v5 vector, round-trips v6/v7 timestamps, fills 1000-id v6 and v7 batches in one native call each, round-trips the SQL-order transforms, and embeds a real-clock v7 timestamp within 2 seconds of the wall clock — not just "it builds."

Target frameworks. Two floors, different on purpose. The package targets net10.0, which is what the native platforms need. WebAssembly is .NET 11 and later only: the exception-handling translation described below is .NET 11 toolchain behavior, and the smoke test targets net11.0 and nothing older. NuGet still imports the package's .targets into a net10.0 Blazor WebAssembly project (a .targets file directly under build/ applies to every target framework), so the wiring is gated on the consuming project's own target framework: below .NET 11 the native core is not linked, and the build says so with warning HYPERUUID001 rather than leaving it to be discovered in the browser. In that configuration UuidGenerator.IsAvailable is false — or, where the wasm-tools workload relinks the runtime, the link stops on undefined uuid_* symbols — so target net11.0, or gate on IsAvailable and keep a managed fallback.

How: one compiled assembly covers every platform, including browser-wasm — no separate build. Every native entry point is declared twice, unconditionally, sharing the same underlying C symbol: once against "hyperuuid" (resolved via dlopen on every real native platform), once against "*" (a statically-linked WASM native has no separate "hyperuuid" module to open, since its functions are already part of the same dotnet.native.wasm the app itself runs in; "*" resolves against the current module instead). OperatingSystem.IsBrowser() picks the right one at each call site — a real runtime check the .NET linker specifically knows how to constant-fold per publish target (the same mechanism the BCL itself uses for platform-conditional code), so a trimmed/published build still only ships the branch that platform can actually reach. Only the Rust core's wasm32-unknown-emscripten static library (cargo rustc --crate-type staticlib — the default cdylib produces an already-linked module NativeFileReference can't pull symbols from) is genuinely RID-specific, landing under runtimes/browser-wasm/nativeassets/net10.0/ in the .nupkg; the managed assembly itself needs no RID-specific copy anymore, confirmed by inspecting a real self-contained dotnet publish -r <rid> output too: no WASM files leak into a non-WASM deployment.

Building this exact source once instead of twice also turned out to matter beyond simplicity: two independent, from-scratch dotnet pack runs produce a byte-identical managed assembly (verified with a real checksum comparison, not assumed) — the earlier two-builds-sharing-one-obj/-directory design never gave that guarantee, and was the leading suspect for this package's NuGet health-check failures on the releases that used it (0.0.5 and earlier).

The one piece that doesn't auto-wire — NuGet's runtimes/{rid}/nativeassets/{tfm}/ convention is real, and the WASM SDK really does auto-promote a resolved NativeLibrary item into NativeFileReference, but restore doesn't actually populate NativeLibrary from a plain PackageReference's nativeassets folder the way it does native/ for ordinary P/Invoke (confirmed empirically, not assumed) — is supplied by this package's own build/HyperUuid.targets, auto-imported into every consuming project via NuGet's standard convention, which adds both the NativeFileReference and (also confirmed necessary the hard way — linking the code in alone does not make it resolvable via "*" at runtime) an explicit EmccExportedFunction entry per native function this package P/Invokes. That's the actual mechanism making this "just works" for real, not a hopeful description of how NuGet packaging is supposed to behave.

HyperUuid.WasmSmokeTest proves this chain in a real browser: a Blazor WebAssembly app (net11.0) that imports the shipped build/HyperUuid.targets, calls every native entry point — the twelve uuid_* functions and hyperuuid_version — through the public UuidGenerator surface, and renders PASS or FAIL. Every one, because that is the only way the check means what it says: an entry point missing from the EmccExportedFunction list links fine and fails only when called. ./check.sh in that directory stages the wasm static library, publishes the app, loads it in headless Chromium and requires PASS. CI runs the same script on every PR, in headless Chrome with the wasm-tools workload, against the static library that run just built (STATICLIB names it, so the script skips building its own); it is not part of the solution, so a plain dotnet build never needs the workload or a browser.

WebAssembly is .NET 11 and later only. .NET 11 links browser-wasm with the new (exnref) exception-handling encoding, while the precompiled Rust standard library inside the static library uses the legacy one, and the browser refuses a module that mixes them (module uses a mix of legacy and new exception handling instructions). The same HyperUuid.targets therefore appends Binaryen's translate-to-exnref pass to the SDK's post-link wasm-opt, with no action needed from a consumer.

Previously reported SDK blocker, not reproduced on .NET 11. Earlier wasm-tools SDK bands failed any native relink with Unknown option '--enable-bulk-memory-opt', a rustc-versus-Binaryen skew filed as dotnet/runtime#132858. On .NET SDK 11.0.100-rc.1 the smoke test relinks cleanly with the SDK's bundled wasm-opt and no workaround. If you hit it on another band, swapping the SDK's bundled wasm-opt (~/.dotnet/packs/Microsoft.NET.Runtime.Emscripten.<version>.Sdk.<rid>/<pack-version>/tools/bin/wasm-opt) for a newer one from a standalone emsdk is the verified workaround.

Platform support

Native binaries ship inside the package for eight RIDs, plus a WebAssembly static library:

Platform RIDs Native asset
Linux (glibc) linux-x64, linux-arm64 libhyperuuid.so
Linux (musl — Alpine) linux-musl-x64, linux-musl-arm64 libhyperuuid.so
macOS osx-x64, osx-arm64 libhyperuuid.dylib
Windows win-x64, win-arm64 hyperuuid.dll
Blazor WebAssembly (.NET 11+) browser-wasm libhyperuuid.a (static — see above)

musl is its own build, not the glibc one relabeled. A glibc libhyperuuid.so does not load under musl's dynamic loader, and NuGet's RID graph falls back from linux-musl-x64 to linux-x64 when nothing more specific is in the package — which is what 0.3.0 and earlier did on Alpine: the glibc library was selected, failed to load, and the first call threw. The musl libraries are built inside Alpine itself and depend on nothing but musl libc. Proven the way a consumer meets it, in an mcr.microsoft.com/dotnet/sdk Alpine container: this binding's whole test suite and the Native AOT smoke test (-r linux-musl-x64) against the musl library, then a throwaway console app consuming the packed .nupkg through a plain PackageReference with the glibc and musl libraries both inside it, which maps runtimes/linux-musl-x64/native/libhyperuuid.so and nothing else. The same app against a glibc-only package is the control: UuidGenerator.IsAvailable is false there, and nothing throws until something ignores it.

On any platform outside that table the package still restores and compiles — the managed assembly is platform-neutral — and UuidGenerator.IsAvailable is how an app finds out at run time that no native library came with it.

Known gap: iOS, Mac Catalyst, and Android are not supported. A .NET MAUI app can reference this package for its Windows and macOS heads, which the RIDs above cover, but not for its mobile heads — the package neither ships those native assets nor declares those target frameworks. Stated here as an explicit gap rather than left for a consumer to discover at link time.

Android is the smaller half: it needs an NDK cross-build added to the release matrix, but resolution is then ordinary dlopen of a .so out of runtimes/{rid}/native/, exactly like the Linux RIDs already do.

Apple mobile is a packaging change, not a matrix row. Native AOT for iOS-like platforms (.NET 9+) does cover ios-arm64, iossimulator-arm64/-x64 and maccatalyst-arm64/-x64 — but a native dependency on those targets is linked statically into the app, via NativeReference with Kind=Static or Native AOT's NativeLibrary/DirectPInvoke, rather than resolved at runtime from runtimes/{rid}/native/. That is structurally the same problem the WebAssembly support above already solves: build the Rust core as a .a rather than a shared library, and let this package's own auto-imported build/HyperUuid.targets inject the reference so a consumer still writes nothing but a PackageReference. The packaging mechanism is therefore already proven in this repo; what is not yet established is how the managed LibraryImport declaration should resolve against a statically-linked core on iOS, which is the first thing to settle whenever this is picked up.

Native binary provenance

The .nupkg carries compiled native code, which is a real thing to ask questions about before adopting it inside a trust boundary. What is and isn't currently guaranteed:

Where the binaries come from. Nothing under csharp/HyperUuid/runtimes/ is committed — it's .gitignored. The native libraries are built from rust/ by CI and staged into the package at pack time, so what ships is produced by the same workflow run that built and tested the source. (The Go, PHP, and Swift bindings are different: those do carry committed binaries, staged by stage-native-binaries.yml, whose commit message records the exact source SHA and CI run ID they came from — e.g. chore: stage native binaries from ci.yml run 33438784689.)

Building it yourself. The core is a normal Rust crate with no build-time codegen, so you never have to take the shipped binary at all:

cd rust && cargo cdylib
# -> target/release/libhyperuuid.so  (.dylib on macOS, hyperuuid.dll on Windows)

Drop the result into csharp/HyperUuid/runtimes/<rid>/native/ and the package's own MSBuild globs will pick it up, or point dlopen at it however you prefer — the C ABI in rust/src/ffi.rs is the entire contract: the twelve uuid_* functions and hyperuuid_version, taking plain pointers into your own buffers. For local development nothing needs dropping anywhere: when no library has been staged under runtimes/ for your machine's RID, the project copies rust/target/release/ straight to the output, so dotnet test after a cargo cdylib just runs.

Reproducibility, stated honestly. The build is deterministic locally: cargo clean -p hyperuuid followed by cargo cdylib reproduces a byte-identical libhyperuuid.so (verified by SHA-256). It is not currently bit-reproducible across machines — a local rustc 1.98.0 build on WSL and the CI-built linux-arm64 artifact differ in both hash and size (458,712 vs 458,176 bytes), as you'd expect from differing toolchain versions and embedded build paths. So "rebuild it and compare hashes" is not a verification path a consumer can currently rely on.

Signed provenance. Because rebuild-and-compare doesn't work across machines, the mechanism that does is a cryptographic attestation binding each artifact to the workflow run and commit that produced it. CI emits SLSA build provenance at three points, because the package is not the same bytes at every stage of its life:

Attested artifact Where How to verify
Each native library, as built hyper-build-native.yml gh attestation verify libhyperuuid.so --repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.github
The .nupkg as packed, pre-push hyper-pack-nuget.yml strip the repo signature first (below)
The .nupkg as published release.yml, after the push verify the downloaded file directly

The reason for the last two rows: nuget.org adds its repository signature as a .signature.p7s entry inside the .nupkg zip during validation, which changes the file's SHA-256. So the package you download is not the package that was built, and one attestation cannot cover both. Rather than pick, the pipeline takes both — and because the mutation is exactly one added zip entry, the pre-push attestation stays recoverable:

# verify the published bytes directly — nothing to undo.
# Signed by release.yml, which lives in this repo, so no --signer-repo is needed.
gh attestation verify HyperUuid.X.Y.Z.nupkg --repo SkunkWerkx/HyperUuid

# or recover the as-built artifact and verify that instead.
# Signed by hyper-pack-nuget.yml over in the forge repo, so this half needs --signer-repo.
zip -d HyperUuid.X.Y.Z.nupkg .signature.p7s
gh attestation verify HyperUuid.X.Y.Z.nupkg \
  --repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.github

Why --signer-repo appears on some of these and not others. --repo X asserts two separate things: that the artifact came from repo X, and that the workflow which signed it also lives in X. Everything CI builds here comes from this repo, so the first half always holds — but the signing step's location varies. Anything signed inside a reusable workflow (hyper-build-native.yml, hyper-pack-nuget.yml) is signed by a file that physically lives in SkunkWerkx/.github, and that is what Fulcio records as the build signer; anything signed directly by this repo's own release.yml is signed by this repo. Get it wrong and gh reports verifying with issuer "sigstore.dev" with no further detail, which reads like a bad signature but is only an identity mismatch. --owner SkunkWerkx works for every row above if you would rather not track which is which.

The release run's job summary prints all three digests — as packed, as published, and as published-with-the-signature-removed — and asserts that the third equals the first. That claim is checked on every release rather than asserted here, so if nuget.org ever changes how it finalizes packages, the run says so instead of the README quietly going stale.

Attestations are produced on pushes, releases, and same-repo pull requests. Only pull requests from forks go unattested, because a fork's token can't sign — so a fork PR legitimately has none, while a branch PR in this repo does. The post-publish half is non-blocking: the push is irreversible, so a slow nuget.org validation is never allowed to turn a successful publish into a failed release.

Not currently done: NuGet author signing. The package carries nuget.org's repository signature but no author signature of our own, which would need an X.509 code-signing certificate registered to the account. It's complementary to the above rather than a substitute, and the difference is who does the checking: an author signature is verified automatically by every consumer's SDK at restore time, whereas an attestation is only checked by someone who deliberately runs gh attestation verify. Attestation ties an artifact to a commit and a build; an author signature ties it to an identity. If you want the automatic restore-time check, this is the gap.

Per-platform AOT receipts. The same CI run publishes HyperUuid.AotSmokeTest under Native AOT on five desktop RIDs (every one but osx-x64, which is cross-built and has no CI leg) and, in Alpine containers, on the two musl ones, fails the build on any ILxxxx/AOTxxxx trim diagnostic, executes the resulting binary, and requires exit 0. Each leg's log uploads as an aot-report-{rid} artifact.

Install

Published to nuget.org — no extra package source needed:

dotnet add package HyperUuid

Per-RID native libraries ship inside the package under runtimes/, so a consumer adds one reference and nothing else — no build step, no manual native staging. See Platform support for the list.

Targets net10.0 for the native platforms; Blazor WebAssembly needs .NET 11 or later — see WebAssembly (Blazor).

See the repo root README for the full RFC 9562 coverage table and the state of every other language binding.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.9.0 141 10/20/2026
0.8.0 267 10/9/2026
0.7.0 114 10/7/2026
0.6.1 120 10/3/2026
0.6.0 118 10/3/2026
0.5.0 125 10/2/2026
0.4.0 117 10/2/2026
0.3.0 137 9/4/2026
0.2.1 126 9/2/2026
0.2.0 122 9/2/2026
0.1.1 111 8/31/2026
0.1.0 110 8/30/2026