Skip to content

About

Source-generated, zero-allocation specification pattern for .NET 8+. Compose specifications at compile time with no heap allocation on the evaluation path. AOT-safe.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

172 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZeroAlloc.Specification

NuGet Build codecov License: MIT AOT GitHub Sponsors

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, Or and Not build readonly struct values. Composing and calling IsSatisfiedBy allocates 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 a WHERE clause.
  • Works under NativeAOT and trimming, with a NativeAOT smoke app published and run in CI. Mistakes such as [Specification] on a class or a missing partial are 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, Or and Not, nested to any depth, without allocating.
  • Static builder: Spec.And, Spec.Or and Spec.Not when 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 struct implementing ISpecification<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

About

Source-generated, zero-allocation specification pattern for .NET 8+. Compose specifications at compile time with no heap allocation on the evaluation path. AOT-safe.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages