ZeroAlloc.Specification
1.3.2
dotnet add package ZeroAlloc.Specification --version 1.3.2
NuGet\Install-Package ZeroAlloc.Specification -Version 1.3.2
<PackageReference Include="ZeroAlloc.Specification" Version="1.3.2" />
<PackageVersion Include="ZeroAlloc.Specification" Version="1.3.2" />
<PackageReference Include="ZeroAlloc.Specification" />
paket add ZeroAlloc.Specification --version 1.3.2
#r "nuget: ZeroAlloc.Specification, 1.3.2"
#:package ZeroAlloc.Specification@1.3.2
#addin nuget:?package=ZeroAlloc.Specification&version=1.3.2
#tool nuget:?package=ZeroAlloc.Specification&version=1.3.2
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 320× faster for in-memory filtering than Ardalis.Specification, measured in the same run: 100 items in 49 ns instead of 15,599 ns, because nothing is compiled per call.
- 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.
| Operation | Ardalis.Specification | ZA.Specification | Speedup |
|---|---|---|---|
| Construct composed spec | 305 ns / 792 B | < 1 ns / 0 B | > 300× faster, 0 B alloc |
| Compose two specs | 821 ns / 1,752 B | < 1 ns / 0 B | > 800× faster, 0 B alloc |
| Evaluate over 100 items (in-memory) | 15,599 ns / 4,743 B | 49 ns / 0 B | ~320× faster, 0 B alloc |
Rows under 1 ns are below what BenchmarkDotNet can tell apart from an empty method, so those speedups are lower bounds. The machine was shared while measuring, so timings are approximate; the allocations are exact. 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.