![]() |
VOOZH | about |
dotnet add package ZeroAlloc.ValueObjects --version 1.7.1
NuGet\Install-Package ZeroAlloc.ValueObjects -Version 1.7.1
<PackageReference Include="ZeroAlloc.ValueObjects" Version="1.7.1" />
<PackageVersion Include="ZeroAlloc.ValueObjects" Version="1.7.1" />Directory.Packages.props
<PackageReference Include="ZeroAlloc.ValueObjects" />Project file
paket add ZeroAlloc.ValueObjects --version 1.7.1
#r "nuget: ZeroAlloc.ValueObjects, 1.7.1"
#:package ZeroAlloc.ValueObjects@1.7.1
#addin nuget:?package=ZeroAlloc.ValueObjects&version=1.7.1Install as a Cake Addin
#tool nuget:?package=ZeroAlloc.ValueObjects&version=1.7.1Install as a Cake Tool
👁 NuGet
👁 Build
👁 AOT
👁 GitHub Sponsors
Zero-allocation source-generated ValueObject equality for your existing domain types.
Same performance as record — without forcing the record keyword on your domain model. Add [ValueObject] to any partial class or partial struct and the generator emits Equals, GetHashCode, ==, !=, and ToString with no heap allocations.
dotnet add package ZeroAlloc.ValueObjects
// Annotate any existing partial class — no keyword changes, no base class
[ValueObject]
public partial class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency) => (Amount, Currency) = (amount, currency);
}
// Use standard equality — zero allocations
var a = new Money(10m, "USD");
var b = new Money(10m, "USD");
bool equal = a == b; // true
bool same = a.Equals(b); // true — IEquatable<Money> fast path
int hash = a.GetHashCode(); // same as b.GetHashCode() — safe as dict key
string s = a.ToString(); // "Money { Amount = 10, Currency = USD }"
Multi-field value objects (Money(decimal Amount, string Currency)): ZA matches record / record struct exactly. CFE allocates ~90 B per call.
| Method | Mean | Allocated |
|---|---|---|
| CFE_Equals | 45.2 ns | 96 B |
| Record_Equals | 3.1 ns | 0 B |
| ZeroAllocStruct_Equals | 2.8 ns | 0 B |
| CFE_GetHashCode | 38.7 ns | 88 B |
| ZeroAllocStruct_GetHashCode | 2.2 ns | 0 B |
Single-int wrapped IDs vs Vogen (the source-gen alternative):
| Operation | Vogen | ZA.ValueObjects | Winner |
|---|---|---|---|
From(value) |
4.66 ns | 0.39 ns | ZA 12× faster |
Equals (equal) |
1.15 ns | 0.09 ns | ZA 13× faster |
Equals (not equal) |
0.31 ns | 0.02 ns | ZA 15× faster |
GetHashCode |
0.03 ns | 0.42 ns | parity (both in ZeroMeasurement zone) |
ToString |
6.40 ns | 3.52 ns | ZA 1.8× faster |
ZA wins or ties every row, with 0 B allocated on all of them. The previous regressions on GetHashCode (~30× slower) and ToString (~72 B allocation) were closed in v1.7 by aligning the single-property emit to Value.ToString(InvariantCulture) / Value.GetHashCode() directly. ZA also supports multi-field types — [ValueObject] on records with any number of fields — which Vogen does not.
Full methodology and analysis: docs/performance.md
partial class and partial struct — no refactoring required[EqualityMember] (opt-in) and [IgnoreEqualityMember] (opt-out)with, no Deconstruct, no EqualityContractHashCode.Combine for ≤8 properties, incremental HashCode.Add for 9+record?record |
ZeroAlloc.ValueObjects |
|
|---|---|---|
| Zero allocation | ✓ | ✓ |
Works on existing class/struct |
✗ — forces record keyword |
✓ |
| Can inherit from non-record base | ✗ | ✓ |
| Fine-grained member control | ✗ | [EqualityMember] / [IgnoreEqualityMember] |
| No extra generated members | ✗ — adds EqualityContract, with, deconstruct |
✓ |
| Struct support | record struct |
partial struct |
[TypedId] is the companion attribute for strongly-typed IDs — OrderId, UserId, MessageId. It solves the same problem as the ValueObject attribute, but tailored for single-value identifiers with built-in generation strategies.
using ZeroAlloc.ValueObjects;
[TypedId(Strategy = IdStrategy.Ulid)]
public readonly partial record struct OrderId;
// Usage
OrderId id = OrderId.New(); // monotonic ULID
string s = id.ToString(); // "01ARZ3NDEKTSV4RRFFQ69G5FAV" — 26-char base32
OrderId parsed = OrderId.Parse(s); // round-trip
| Strategy | Backing | Format | Use case |
|---|---|---|---|
Ulid (default) |
Guid |
26-char Crockford base32 | General-purpose, sortable, URL-safe |
Uuid7 |
Guid |
36-char hyphenated UUID | Time-ordered with standard UUID interop |
Snowflake |
long |
Decimal string | Distributed systems needing 64-bit IDs |
Sequential |
long |
Decimal string | Test stability only — not for production |
[assembly: TypedIdDefault(Strategy = IdStrategy.Ulid)]
[TypedId] // resolves to Ulid from the assembly default
public readonly partial record struct ProductId;
[TypedId(Strategy = IdStrategy.Snowflake)] // per-struct override
public readonly partial record struct MessageId;
Snowflake IDs encode a 10-bit worker ID so multiple processes can mint IDs concurrently without collision. Configure at startup:
builder.Services.AddSnowflakeWorkerId(workerId: 5);
builder.Services.AddSnowflakeWorkerId(envVar: "POD_ORDINAL", fallback: 0);
builder.Services.AddSnowflakeWorkerId(sp => sp.GetRequiredService<IMachineIdProvider>().Id);
If no provider is registered, Snowflake.New() falls back to ZA_SNOWFLAKE_WORKER_ID env var, then throws TypedIdException.
Install ZeroAlloc.ValueObjects.EfCore and register the convention:
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
builder.AddTypedIdConventions();
}
All [TypedId] structs in the DbContext's assembly are auto-mapped: Guid-backed → uniqueidentifier/uuid, long-backed → bigint. Per-property HasConversion still overrides.
No setup needed — the generator emits IParsable<T> + ISpanParsable<T>, so app.MapGet("/orders/{id}", (OrderId id) => …) just works.
Each TypedId carries [JsonConverter] pointing at a nested converter that reads/writes a string. Fully AOT-safe. No JsonSerializerContext wiring required for basic use; if you're source-generating JsonSerializerContext, include the TypedId types there too.
| ID | Severity | Meaning |
|---|---|---|
ZATI001 |
Error | Incompatible strategy/backing (e.g. Snowflake + Guid) |
ZATI002 |
Error | Type is not readonly partial record struct |
ZATI003 |
Error | Struct body declares fields — generator owns Value |
ZATI005 |
Warning | Struct declared partial across multiple files |
AddSnowflakeWorkerId cannot detect duplicates. Coordinate via orchestrator ordinals (Kubernetes pod index, Nomad alloc index) or a central registry. Duplicate worker IDs silently produce colliding IDs.TypedIdException. Run NTP-synced or accept the brief unavailability.| Page | Description |
|---|---|
| Why this library? | The problem with CFE, why not just use record |
| Installation | NuGet install, .NET version requirements |
| Getting Started | Step-by-step quickstart with core concepts |
| Attribute Reference | [ValueObject], [EqualityMember], [IgnoreEqualityMember] |
| Member Selection | How properties are chosen for equality |
| Generated Output | Exact code the generator emits |
| Struct vs. Class | When to use each, ForceClass |
| Nullable Properties | Null-safe comparison generation |
| Usage Patterns | Dictionary keys, HashSets, LINQ, EF Core, pattern matching |
| Migration Guide | From CFE ValueObject, from manual equality |
| Performance | Benchmark results and how to run them |
| Design Decisions | Trade-offs, intentional omissions |
| Troubleshooting | Common errors and fixes |
| Testing | Writing unit tests for value object equality |
| Examples | |
| E-Commerce | ProductId, Money, ShippingAddress, Discount |
| Finance | Iban, CurrencyPair, AccountNumber |
| HR / Identity | EmailAddress, EmployeeId, FullName |
| Geospatial | Coordinates, GeoRegion |
| Scheduling | DateRange, TimeSlot |
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 was computed. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 netcoreapp2.0 was computed. netcoreapp2.1 netcoreapp2.1 was computed. netcoreapp2.2 netcoreapp2.2 was computed. netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 netstandard2.0 is compatible. netstandard2.1 netstandard2.1 is compatible. |
| .NET Framework | net461 net461 was computed. net462 net462 was computed. net463 net463 was computed. net47 net47 was computed. net471 net471 was computed. net472 net472 was computed. net48 net48 was computed. net481 net481 was computed. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen40 tizen40 was computed. tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
Showing the top 5 NuGet packages that depend on ZeroAlloc.ValueObjects:
| Package | Downloads |
|---|---|
|
ZeroAlloc.Rest
Source-generated, AOT-compatible REST client for .NET |
|
|
ZeroAlloc.Scheduling
Source-generated zero-allocation background job scheduling for .NET. |
|
|
ZeroAlloc.Outbox
Source-generated transactional outbox for .NET. |
|
|
AI.Sentinel
Security monitoring middleware for IChatClient — prompt injection, hallucination, and operational anomaly detection with an intervention engine. |
|
|
ZeroAlloc.ValueObjects.EfCore
EF Core value converters and conventions for ZeroAlloc.ValueObjects [TypedId] structs. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.7.1 | 344 | 6/11/2026 |
| 1.7.0 | 2,509 | 5/18/2026 |
| 1.6.0 | 332 | 5/13/2026 |
| 1.5.1 | 130 | 5/12/2026 |
| 1.5.0 | 487 | 5/4/2026 |
| 1.4.1 | 257 | 5/3/2026 |
| 1.4.0 | 150 | 5/1/2026 |
| 1.3.3 | 817 | 5/1/2026 |
| 1.3.2 | 327 | 4/29/2026 |
| 1.3.1 | 1,672 | 4/28/2026 |
| 1.2.0 | 8,141 | 4/22/2026 |
| 1.1.6 | 2,807 | 3/20/2026 |
| 1.1.5 | 108 | 3/19/2026 |
| 1.1.4 | 111 | 3/17/2026 |
| 1.1.3 | 108 | 3/16/2026 |
| 1.1.2 | 110 | 3/16/2026 |
| 1.1.1 | 111 | 3/16/2026 |