ZeroAlloc.Specification
1.3.3
dotnet add package ZeroAlloc.Specification --version 1.3.3
NuGet\Install-Package ZeroAlloc.Specification -Version 1.3.3
<PackageReference Include="ZeroAlloc.Specification" Version="1.3.3" />
<PackageVersion Include="ZeroAlloc.Specification" Version="1.3.3" />
<PackageReference Include="ZeroAlloc.Specification" />
paket add ZeroAlloc.Specification --version 1.3.3
#r "nuget: ZeroAlloc.Specification, 1.3.3"
#:package ZeroAlloc.Specification@1.3.3
#addin nuget:?package=ZeroAlloc.Specification&version=1.3.3
#tool nuget:?package=ZeroAlloc.Specification&version=1.3.3
ZeroAlloc.Specification
Source-generated specification pattern for .NET, an Ardalis.Specification alternative: specs are structs, so composing and evaluating them allocates nothing, and the same spec still translates to SQL through EF Core.
Why ZeroAlloc.Specification
- Composing and evaluating allocates nothing.
And,OrandNotbuildreadonly structvalues. Composing and callingIsSatisfiedByallocates 0 B, asserted by tests. Ardalis.Specification allocates 1,752 B to compose two specs. - About 630× faster for in-memory filtering than Ardalis.Specification, measured in the same run over 100 items, because nothing is compiled per call, and it allocates nothing against 4,743 B.
- One spec for memory and the database. Every spec also exposes
ToExpression(), so EF Core turns the composed spec into aWHEREclause. - Works under NativeAOT and trimming, with a NativeAOT smoke app published and run in CI. Mistakes such as
[Specification]on a class or a missingpartialare compile errors ZA001 to ZA003.
Install
dotnet add package ZeroAlloc.Specification
The source generator ships inside this package. Targets .NET 8 and later.
Quick start
using System.Linq.Expressions;
using ZeroAlloc.Specification;
User[] users = [new("Ann", true, 1500m), new("Bob", true, 200m), new("Cy", false, 3000m)];
var spec = new ActiveUserSpec().And(new BigSpenderSpec(1000m)); // a struct, 0 B
Console.WriteLine(users.Count(u => spec.IsSatisfiedBy(u))); // 1, in memory
Console.WriteLine(spec.ToExpression()); // one combined expression for dbContext.Users.Where(...)
public sealed record User(string Name, bool IsActive, decimal TotalSpend);
[Specification]
public readonly partial struct ActiveUserSpec : ISpecification<User>
{
public bool IsSatisfiedBy(User user) => user.IsActive;
public Expression<Func<User, bool>> ToExpression() => u => u.IsActive;
}
[Specification]
public readonly partial struct BigSpenderSpec(decimal minSpend) : ISpecification<User>
{
public bool IsSatisfiedBy(User user) => user.TotalSpend >= minSpend;
public Expression<Func<User, bool>> ToExpression() { var min = minSpend; return u => u.TotalSpend >= min; }
}
The generator adds And, Or and Not to every [Specification] struct. Evaluation is a direct call on a struct that the JIT can inline, with no virtual dispatch.
What you get
- Fluent composition:
And,OrandNot, nested to any depth, without allocating. - Static builder:
Spec.And,Spec.OrandSpec.Notwhen you want explicit type arguments. - In-memory evaluation through
IsSatisfiedBy, for hot loops and domain rules. - EF Core translation through
ToExpression(), which combines the specs' expression trees into one. - Compile-time checks that each spec is a
readonly partial structimplementingISpecification<T>. - Recipes for an EF Core repository, complex compositions and caching the expression of a stateless spec.
Building an expression tree with ToExpression() does allocate, as every expression tree does. Cache it for stateless specs.
Performance
Head-to-head vs Ardalis.Specification 9.3.1, .NET 10.0.13, BenchmarkDotNet v0.15.8. Allocations are exact; speed is a same-run ratio.
| Operation | Ardalis.Specification time ÷ ZA's | Ardalis alloc | ZA alloc |
|---|---|---|---|
| Construct composed spec | ZA below resolution | 792 B | 0 B |
| Compose two specs | ZA below resolution | 1,752 B | 0 B |
| Evaluate over 100 items (in-memory) | 630× (632–633) | 4,743 B | 0 B |
ZA's construct and compose are below what BenchmarkDotNet can tell apart from an empty method, so no finite ratio can be given for them. For database queries the SQL provider dominates and the gap is invisible; for in-memory filtering it dominates. Full methodology: docs/performance.md.
Learn more
- Documentation: specification.zeroalloc.net. Start with getting started, core concepts and the diagnostics reference.
- Release history: CHANGELOG.md.
- More ZeroAlloc libraries, such as ZeroAlloc.Validation and ZeroAlloc.Mapping: zeroalloc.net.
- Issues and pull requests are welcome on GitHub.
- MIT licensed. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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. |
-
net8.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on ZeroAlloc.Specification:
| Package | Downloads |
|---|---|
|
Rag.NET.Abstractions
Interfaces and models for the Rag.NET library |
|
|
Rag.NET
Modular RAG pipeline library for .NET — ingestion and retrieval pipeline builders, self-query, corrective RAG and conversation memory |
GitHub repositories
This package is not used by any popular GitHub repositories.