ZeroAlloc.ValueObjects
1.4.0
dotnet add package ZeroAlloc.ValueObjects --version 1.4.0
NuGet\Install-Package ZeroAlloc.ValueObjects -Version 1.4.0
<PackageReference Include="ZeroAlloc.ValueObjects" Version="1.4.0" />
<PackageVersion Include="ZeroAlloc.ValueObjects" Version="1.4.0" />
<PackageReference Include="ZeroAlloc.ValueObjects" />
paket add ZeroAlloc.ValueObjects --version 1.4.0
#r "nuget: ZeroAlloc.ValueObjects, 1.4.0"
#:package ZeroAlloc.ValueObjects@1.4.0
#addin nuget:?package=ZeroAlloc.ValueObjects&version=1.4.0
#tool nuget:?package=ZeroAlloc.ValueObjects&version=1.4.0
ZeroAlloc.ValueObjects
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.
Install
dotnet add package ZeroAlloc.ValueObjects
Quick start
// 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 }"
Performance
ZeroAlloc.ValueObjects matches record and record struct performance exactly. The only allocating variant is CSharpFunctionalExtensions.ValueObject.
| Method | Mean | Allocated |
|---|---|---|
| CFE_Equals | 45.2 ns | 96 B |
| Record_Equals | 3.1 ns | 0 B |
| RecordStruct_Equals | 2.8 ns | 0 B |
| ZeroAlloc_Equals | 3.1 ns | 0 B |
| ZeroAllocStruct_Equals | 2.8 ns | 0 B |
| CFE_GetHashCode | 38.7 ns | 88 B |
| Record_GetHashCode | 2.4 ns | 0 B |
| RecordStruct_GetHashCode | 2.2 ns | 0 B |
| ZeroAlloc_GetHashCode | 2.4 ns | 0 B |
| ZeroAllocStruct_GetHashCode | 2.2 ns | 0 B |
Full methodology and more scenarios: docs/performance.md
Features
- Zero allocations — no iterator state machine, no boxing
- Works on existing
partial classandpartial struct— no refactoring required - Can inherit from non-record base classes
- Fine-grained member control with
[EqualityMember](opt-in) and[IgnoreEqualityMember](opt-out) - No extra generated members — no
with, noDeconstruct, noEqualityContract - Null-safe comparison for nullable reference type properties
HashCode.Combinefor ≤8 properties, incrementalHashCode.Addfor 9+
Why not just use 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 |
Typed identifiers
[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.
Basic usage
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
Strategies
| 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-level default
[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 worker ID configuration
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.
EF Core
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.
Minimal API binding
No setup needed — the generator emits IParsable<T> + ISpanParsable<T>, so app.MapGet("/orders/{id}", (OrderId id) => …) just works.
JSON
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.
Compile-time diagnostics
| 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 |
Production checklist
- Sequential is not for production. The counter resets on process restart. Use it only in tests where deterministic IDs matter.
- Snowflake worker IDs must be unique across all producing processes.
AddSnowflakeWorkerIdcannot detect duplicates. Coordinate via orchestrator ordinals (Kubernetes pod index, Nomad alloc index) or a central registry. Duplicate worker IDs silently produce colliding IDs. - Clock skew matters. Snowflake generation handles small rollbacks by pinning to the last observed millisecond, but severe skew (>5s) throws
TypedIdException. Run NTP-synced or accept the brief unavailability. - Process restart loses Sequential state but not Snowflake or ULID/UUID7 ordering. ULID/UUID7 are globally safe to restart; Snowflake is safe if worker ID is stable across restarts.
Documentation
| 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 |
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- System.Memory (>= 4.5.5)
- System.Security.Cryptography.Algorithms (>= 4.3.1)
NuGet packages (6)
Showing the top 5 NuGet packages that depend on ZeroAlloc.ValueObjects:
| Package | Downloads |
|---|---|
|
ZeroAlloc.Scheduling
Source-generated zero-allocation background job scheduling for .NET. |
|
|
ZeroAlloc.Outbox
Source-generated transactional outbox for .NET. |
|
|
ZeroAlloc.Rest
Source-generated, AOT-compatible REST client 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. |
GitHub repositories
This package is not used by any popular GitHub repositories.