Skip to main content

Expression Composition

ToExpression() returns an Expression<Func<T, bool>> that can be passed directly to EF Core, LINQ-to-SQL, or any other IQueryable provider.

How It Works

When composing two expressions, both sides must share a single ParameterExpression object — EF Core requires this for correct SQL translation.

ZeroAlloc.Specification uses an internal ParameterRebinder (an ExpressionVisitor) to replace the right-hand expression's parameter with the left-hand expression's parameter before combining bodies:

// AndSpecification<TLeft, TRight, T>.ToExpression()
var left = _left.ToExpression();
var right = _right.ToExpression();
var param = left.Parameters[0];
var rightBody = ParameterRebinder.ReplaceParameter(right.Body, right.Parameters[0], param);
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left.Body, rightBody), param);

The resulting expression tree uses Expression.AndAlso / Expression.OrElse / Expression.Not — all translatable to SQL by EF Core.

EF Core Usage

var spec = new ActiveUserSpec().And(new PremiumUserSpec(1000m));

// Works with any IQueryable provider
var users = await dbContext.Users
.Where(spec.ToExpression())
.ToListAsync();

Expression Tree Structure

For ActiveUserSpec.And(PremiumUserSpec):

Lambda (u =>
AndAlso(
u.IsActive,
u.TotalSpend >= 1000
)
)

Both u.IsActive and u.TotalSpend reference the same ParameterExpression u.

Stateless Expression Caching

For stateless specs (no instance fields), you can cache the expression as a static field:

public readonly partial struct ActiveUserSpec : ISpecification<User>
{
private static readonly Expression<Func<User, bool>> _cachedExpression =
new ActiveUserSpec().ToExpression();

public Expression<Func<User, bool>> ToExpression() => _cachedExpression;
}

This avoids rebuilding the expression tree on every call. See Stateless Caching for the full pattern.

Stateful Expressions

For stateful specs, do not cache — the expression captures a specific value:

public readonly partial struct PremiumUserSpec : ISpecification<User>
{
private readonly decimal _minSpend;

public Expression<Func<User, bool>> ToExpression()
{
var min = _minSpend; // copy field to local — required for struct lambdas
return u => u.TotalSpend >= min;
}
}