Skip to content

About

Zero-allocation source-generated ValueObject equality for hot paths. Eliminates boxing and iterator allocations from CSharpFunctionalExtensions.ValueObject.GetEqualityComponents().

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

275 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZeroAlloc.ValueObjects

NuGet Build codecov License: MIT AOT GitHub Sponsors

Source-generated value objects and strongly-typed IDs for .NET: record-speed equality on the classes and structs you already have, with no allocation and no base class, NativeAOT-ready.

Why ZeroAlloc.ValueObjects

  • Equality allocates nothing. Equals, GetHashCode, == and != allocate 0 B on a class and on a struct, asserted by tests. CSharpFunctionalExtensions' ValueObject allocates 144 B per Equals in the same benchmark.
  • As fast as record, without rewriting your types to records. Equality on a two-field value object runs in the same time as record and 444× faster than CSharpFunctionalExtensions. On a single-int value object it runs at parity with Vogen.
  • Works under NativeAOT and trimming. No reflection in generated code, including the EF Core converter registration. A NativeAOT smoke app is published and run in CI.
  • Mistakes fail the build. An ID strategy that does not fit its backing type, such as Snowflake on a Guid, is compile error ZATI001.

Install

dotnet add package ZeroAlloc.ValueObjects

Quick start

using ZeroAlloc.ValueObjects;

var price = new Money(10m, "EUR");
var prices = new HashSet<Money> { price, new Money(10m, "EUR") };
Console.WriteLine(price == new Money(10m, "EUR"));       // True: compared by value, 0 B
Console.WriteLine(prices.Count);                         // 1: safe as a set or dictionary key

OrderId id = OrderId.New();                              // sortable ULID
Console.WriteLine($"{id} {OrderId.Parse(id.ToString()) == id}");

[ValueObject]
public partial class Money
{
    public decimal Amount { get; }
    public string Currency { get; }
    public Money(decimal amount, string currency) => (Amount, Currency) = (amount, currency);
}

[TypedId(Strategy = IdStrategy.Ulid)]
public readonly partial record struct OrderId;

Money keeps its constructor, its base class and its shape. The generator adds the equality members and a ToString, without the with, Deconstruct and EqualityContract a record brings.

What you get

  • Value objects on existing types: [ValueObject] on any partial class or partial struct, including classes that inherit from a non-record base.
  • Member control: opt members in with [EqualityMember] or out with [IgnoreEqualityMember]. Nullable reference properties compare null-safely.
  • Strongly-typed IDs: [TypedId] with ULID, UUIDv7, Snowflake and Sequential strategies, plus an assembly-wide default.
  • JSON through a generated System.Text.Json converter that reads and writes with 0 B allocated, asserted by tests.
  • ASP.NET Core binding: IDs implement IParsable<T> and ISpanParsable<T>, so (OrderId id) => … route parameters just work.
  • EF Core: ZeroAlloc.ValueObjects.EfCore maps every typed ID in an assembly with one generated, trim-safe call.
  • Snowflake worker IDs from a number, an environment variable or a factory. ZeroAlloc.ValueObjects.Hosting adds a factory that resolves services.
  • Serialisation: ZeroAlloc.Serialisation writes single-value objects as their underlying value in JSON, MessagePack and MemoryPack.

Performance

BenchmarkDotNet. Allocations are exact. The time column is the alternative's time divided by ZeroAlloc.ValueObjects', from the same run.

Operation ZeroAlloc.ValueObjects Alternative Alternative's time
Equals, two-field Money 0 B CSharpFunctionalExtensions, 144 B 444×
GetHashCode, two-field Money 0 B CSharpFunctionalExtensions, 72 B on the first call per instance, then cached —
Equals, single-int value object 0 B Vogen, 0 B both below resolution

.NET 10.0.13, CSharpFunctionalExtensions 3.7.0, Vogen 8.0.7, measured 2026-10-21. record and record struct allocate 0 B and run in the same time as ZeroAlloc.ValueObjects on two-field Equals. The performance page has every row, including GetHashCode, where record is faster, and the typed-ID benchmarks cover generation and parsing per strategy.

Learn more

About

Zero-allocation source-generated ValueObject equality for hot paths. Eliminates boxing and iterator allocations from CSharpFunctionalExtensions.ValueObject.GetEqualityComponents().

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages