Troubleshooting
Practical problem/solution guide for Pragmatic.Specification. Each section covers a common issue, the likely causes, and the fix.
Specification Not Filtering (All Results Returned)
Section titled “Specification Not Filtering (All Results Returned)”Your specification compiles and runs, but the query returns all rows instead of the filtered subset.
Checklist
Section titled “Checklist”-
Did you import the extensions namespace? Without
using Pragmatic.Specification.Extensions;, the standard LINQWhereis used instead of the specification-aware overload. The compiler may not warn you becauseWhere(spec)could be interpreted asWhere(Func<T, bool>)via implicit conversion in some edge cases. -
Are you using
Spec<T>.Truewithout further composition?Spec<T>.Truematches everything. If yourAndIfconditions are allfalse, the spec remainsTrue. -
Is the specification applied to the right
DbSet? Double-check that you are querying the correct entity type. -
Is the predicate logic correct? Test with
IsSatisfiedByagainst a known entity:var order = new Order { IsCancelled = true };bool result = OrderSpecs.Active.IsSatisfiedBy(order);// If result is true, the predicate logic is wrong
EF Core Cannot Translate the Expression
Section titled “EF Core Cannot Translate the Expression”You get InvalidOperationException: The LINQ expression 'xxx' could not be translated at runtime.
Possible Causes
Section titled “Possible Causes”Custom method call in the expression. EF Core can only translate known .NET methods to SQL. If your specification calls a custom method, EF Core cannot translate it:
// This will fail at runtime with EF CoreSpec<Order>.Where(o => CalculateAge(o.CreatedAt) > 30)Fix: Inline the logic directly in the lambda, using only EF Core-translatable operations:
Spec<Order>.Where(o => o.CreatedAt < DateTimeOffset.UtcNow.AddDays(-30))Unsupported string operations. Not all string methods are supported by all providers. For example, string.Format is generally not translatable.
Provider-specific limitations. Some expressions translate on PostgreSQL but not SQL Server, or vice versa. Check your provider’s documentation for supported translations.
Debugging Strategy
Section titled “Debugging Strategy”- Extract the expression with
spec.ToExpression()and inspect it. - Try the lambda directly in a
.Where()call to confirm EF Core can translate it. - If the expression is a composition, test each part individually to find which one fails.
Composed Specification Produces Wrong SQL
Section titled “Composed Specification Produces Wrong SQL”The AND/OR composition generates unexpected results in SQL.
Possible Causes
Section titled “Possible Causes”Operator precedence. Without parentheses, & binds tighter than |:
// This means: A OR (B AND C) -- not (A OR B) AND Cvar spec = specA | specB & specC;Fix: Use explicit parentheses:
var spec = (specA | specB) & specC;Stale cached delegate. If you modified a specification class but the old compiled delegate is cached (e.g., in a static field), you may see stale results in tests. This is rare in production but can happen in test suites that reuse static instances.
IsSatisfiedBy Returns Unexpected Results
Section titled “IsSatisfiedBy Returns Unexpected Results”The specification works correctly with EF Core but returns wrong results when testing with IsSatisfiedBy.
Possible Causes
Section titled “Possible Causes”Date/time evaluation differences. Expressions like DateTimeOffset.UtcNow are captured at different times. In EF Core, the value is translated to GETUTCDATE() (evaluated at query time). In IsSatisfiedBy, it is evaluated at delegate compilation time and cached.
// ToExpression captures "now" at compilation time for in-memory evaluationpublic sealed class RecentOrdersSpec : Specification<Order>{ public override Expression<Func<Order, bool>> ToExpression() { var cutoff = DateTimeOffset.UtcNow.AddDays(-30); return o => o.CreatedAt >= cutoff; }}Fix for time-sensitive tests: Create a new specification instance for each test to get a fresh UtcNow value. Or use Pragmatic.Temporal.IClock for deterministic time in tests.
Entity state differences. The entity you pass to IsSatisfiedBy may have different property values than what exists in the database. Verify the entity state matches your expectations.
Performance: Specification Creates Too Many Allocations
Section titled “Performance: Specification Creates Too Many Allocations”You are composing many specifications in a hot loop and seeing allocation pressure.
Recommendations
Section titled “Recommendations”-
Cache parameterless specifications as static properties:
// One allocation, reused foreverpublic static Specification<Order> Active { get; } =Spec<Order>.Where(o => !o.IsCancelled && !o.IsDeleted); -
Pre-compose common combinations:
public static Specification<Order> ActiveHighValue { get; } =Active & HighValue(1000m); -
Avoid composing specifications inside tight loops. Build the specification once before the loop, not inside it.
-
For truly hot paths, consider using the expression directly instead of creating specification objects. Specifications are designed for business logic clarity, not nanosecond-level performance.
Extension Method Ambiguity
Section titled “Extension Method Ambiguity”The compiler reports an ambiguous call between System.Linq.Queryable.Where and SpecificationExtensions.Where.
This happens when the specification type is ISpecification<T> instead of Specification<T>. The compiler sees ISpecification<T> as a potential Func<T, bool> candidate.
Ensure your variable is typed as Specification<T>:
// May cause ambiguityISpecification<Order> spec = OrderSpecs.Active;db.Orders.Where(spec); // Ambiguous
// No ambiguitySpecification<Order> spec = OrderSpecs.Active;db.Orders.Where(spec); // Resolves to SpecificationExtensions.WhereCan I use specifications with Dapper or raw SQL?
Section titled “Can I use specifications with Dapper or raw SQL?”Not directly. Specifications produce expression trees, which are designed for LINQ query providers (EF Core, LINQ to DB). For Dapper, extract the predicate logic into a shared method and build both the specification and the SQL WHERE clause from it.
Can I compose specifications across different entity types?
Section titled “Can I compose specifications across different entity types?”No. Specification<T> is generic — Specification<Order> cannot compose with Specification<Customer>. Each specification type is bound to a single entity. For cross-entity filtering, compose at the query level using navigation properties.
Is there a performance penalty for composed specifications vs. inline lambdas?
Section titled “Is there a performance penalty for composed specifications vs. inline lambdas?”For IQueryable (EF Core): no. The composed expression tree produces the same SQL as a hand-written lambda with && and ||. For IEnumerable: the compiled delegate has one extra level of function calls, which is negligible. The expression tree composition itself (creating AndSpecification, OrSpecification) allocates a few objects, but this is a one-time cost per composition, not per evaluation.
Can I serialize specifications?
Section titled “Can I serialize specifications?”No. Specifications contain expression trees, which are not serializable. If you need to send filter criteria across a network boundary, use a DTO (like OrderFilterRequest) and reconstruct the specification on the receiving side.
Do specifications work with async methods?
Section titled “Do specifications work with async methods?”IsSatisfiedBy is synchronous because it evaluates a compiled delegate in memory. For async queries, use the extension methods with IQueryable:
var results = await db.Orders.Where(spec).ToListAsync(ct);The extension methods chain with standard LINQ operators, which support async via EF Core’s ToListAsync, CountAsync, etc.
Getting Help
Section titled “Getting Help”| Resource | Location |
|---|---|
| Concepts and architecture | concepts.md |
| Composition patterns | composition-patterns.md |
| Common mistakes | common-mistakes.md |
| Module README | ../README.md |
| Pragmatic.Persistence integration | ../../Pragmatic.Persistence/README.md |