The Specification Pattern in C# sounds simple: put a condition in an object and ask whether a candidate satisfies it. That description is useful, but modern .NET codebases use the word "specification" for three related models. One represents a pure domain rule. Another exposes provider-visible query criteria. A third carries an entire query recipe.
Those models solve different problems. Mixing them creates confusing APIs, unclear ownership, and false expectations about what can be reused. This guide establishes the broad definition, shows where the three models separate, and gives you a balanced way to decide whether the pattern belongs in your design. It stays deliberately small on implementation. The goal is understanding first.
The "modern .NET" baseline here is stable .NET 10 and C# 14; EF Core discussion uses EF Core 10.0.10. Preview releases are outside this guide.
What Is the Specification Pattern in C#?
A specification gives a meaningful name to a condition. Instead of scattering customer.IsActive && customer.CreditLimit >= order.Total across services, you can represent the business idea with a name such as CustomerCanPlaceInvoiceOrder.
The important part is not the class or interface. It is the shift in vocabulary. A raw Boolean expression says how a condition is calculated. A specification says what that condition means to the domain or use case.
The official DDD Reference presents Specification as a domain pattern. The original Evans and Fowler Specification paper describes a specification as an encapsulated predicate and discusses selection, validation, and building to order. That historical center is narrower than many modern .NET libraries. It starts with a named predicate over a candidate.
The .NET platform already has a predicate concept. Predicate<T> is a delegate that returns a Boolean value for an input. A specification can contain equivalent behavior, but it adds identity and domain language. It may also add composition or metadata, depending on the model you choose.
If you want broader context before going deeper, The Big List of Design Patterns -- Everything You Need to Know provides a wider map of pattern families and intent.
The Historical Boundary: Specification Is Not a GoF Pattern
The word "pattern" often causes an immediate association with the Gang of Four. That association is understandable, but it is not accurate here.
Specification is not one of the 23 patterns in the Gang of Four catalog. It is more closely associated with Domain-Driven Design and enterprise application design. That history matters because it explains why the pattern begins with business meaning rather than object-creation mechanics or UI structure.
You may still see familiar GoF mechanics inside an implementation. A tree of AND, OR, and NOT specifications can resemble Composite. A replaceable evaluator can resemble Strategy. Those structural similarities do not change the pattern's primary intent: a specification names criteria that a candidate may or may not satisfy.
This distinction also keeps the pattern from becoming a vague label for any class containing a conditional. If the object has no meaningful rule identity, no reuse, and no reason to exist outside one local method, an inline expression may communicate the design more directly.
Three Specification Models in Modern .NET
The safest way to discuss the Specification Pattern in C# is to name the model before discussing benefits or code. The three models overlap, but they are not interchangeable.
Model 1: Pure Domain Specification
A pure domain specification answers a Boolean question in memory:
Does this candidate satisfy this named business condition?
Its typical surface is bool IsSatisfiedBy(T candidate). It should be deterministic and side-effect free. Given the same unchanged candidate, it should return the same answer without performing I/O, mutating state, or triggering an action.
Examples include:
- An order is eligible for expedited handling.
- A customer can use invoice terms.
- A subscription qualifies for renewal.
- A proposed discount is within a manager's approval range.
This model is a good fit when the rule deserves domain language, appears in more than one decision, or needs focused tests. It does not need Entity Framework Core, a Repository, or a third-party package.
Model 2: Expression or Query Specification
An expression specification stores criteria in a form a query provider can inspect. In C#, that commonly means Expression<Func<T, bool>>.
Queryable.Where accepts an expression tree, while Enumerable.Where accepts a delegate. That difference is fundamental. A delegate can run in memory. An expression tree is data that a provider can inspect and potentially translate.
This model answers a different question:
Which candidates should a provider-backed query select?
The rule may look similar to its domain counterpart, but its operating constraints differ. Provider-visible criteria must stay representable as an expression. The provider decides what it can translate. A rule that works as ordinary C# is not automatically translatable by every EF Core provider.
If expression mechanics are new territory, Expression Trees as a Reflection Alternative in C#: When To Switch offers useful background. For the wider query vocabulary, LINQ in C#: Complete Guide to Language Integrated Query .NET 6-9 covers the operators and execution model that specifications often build upon.
Model 3: Full Query Envelope
A full query envelope goes beyond a predicate. It may describe filtering, search, ordering, related-data loading, projection, tracking behavior, pagination, query tags, cache metadata, and post-processing.
The Ardalis.Specification 9.3.1 interface carries filters, ordering, includes, search criteria, paging, projection-related state, query flags, cache metadata, and post-processing state. That is a valid package-specific design, but it is broader than the historical predicate-only pattern.
This model answers:
What complete query shape should produce the result for this use case?
The distinction becomes important when people say that specifications are composable. Combining two Boolean predicates has familiar semantics. Combining two full query envelopes does not. Which ordering wins? Can two projections coexist? Should page sizes be added, replaced, or rejected? What happens to conflicting tracking modes?
Those questions do not make query envelopes bad. They show why a full query description needs explicit semantics for every field rather than inheriting the simple algebra of Boolean predicates.
A Small Conceptual C# Implementation
The following file contrasts a pure domain specification with an expression criterion. It is intentionally small. It does not implement Boolean composition, persistence, query execution, or a package-specific evaluator.
using System;
using System.Linq.Expressions;
namespace SpecificationPatternGuide;
public interface IDomainSpecification<in T>
{
bool IsSatisfiedBy(T candidate);
}
public sealed class Customer
{
public Customer(bool isActive, decimal creditLimit)
{
if (creditLimit < 0)
{
throw new ArgumentOutOfRangeException(nameof(creditLimit));
}
IsActive = isActive;
CreditLimit = creditLimit;
}
public bool IsActive { get; }
public decimal CreditLimit { get; }
}
public sealed class CustomerCanPlaceInvoiceOrderSpecification
: IDomainSpecification<Customer>
{
private readonly decimal _minimumCreditLimit;
public CustomerCanPlaceInvoiceOrderSpecification(decimal minimumCreditLimit)
{
if (minimumCreditLimit < 0)
{
throw new ArgumentOutOfRangeException(nameof(minimumCreditLimit));
}
_minimumCreditLimit = minimumCreditLimit;
}
public bool IsSatisfiedBy(Customer candidate)
{
ArgumentNullException.ThrowIfNull(candidate);
return candidate.IsActive &&
candidate.CreditLimit >= _minimumCreditLimit;
}
}
public interface IQuerySpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
}
public sealed class ActiveCustomerQuerySpecification
: IQuerySpecification<Customer>
{
public Expression<Func<Customer, bool>> Criteria { get; } =
customer => customer.IsActive;
}
The first rule is ordinary domain logic. It can be evaluated directly and gives a business condition a stable name. The second object exposes an expression for a provider-backed query. They happen to share the Customer type, but they have different contracts and different proof requirements.
That separation is useful even if your application eventually provides adapters between the models. It forces the design to state whether a rule is being evaluated as C# behavior or interpreted by a query provider.
What the Specification Pattern Does Not Replace
Specifications often sit near other abstractions, so blurred boundaries are common. A brief separation prevents the pattern from claiming responsibilities it does not own.
Repository and Query Object
Repository is a collection-like mediation boundary between the domain and data mapping. A Repository may accept a specification, but the specification does not automatically become the Repository.
A Query Object represents a database query and interprets object-oriented criteria into a query language. A full query-envelope specification can look very similar to a specialized Query Object. The practical label matters less than making the object's responsibilities explicit.
Validation and Invariants
Microsoft's domain-model guidance places aggregate invariants in the domain model and presents Specification plus Notification as an advanced validation option. A Boolean specification can participate in validation, but true or false alone is not a complete structured validation result. Request errors, field messages, invariant enforcement, and state transitions still need clear owners.
Authorization
ASP.NET Core authorization policies use requirements and handlers evaluated for a user and optionally a resource. A specification may narrow a data set, but that query criterion is not a replacement for the application's authorization boundary.
Rules Engines
Microsoft RulesEngine supports workflows, multiple inputs, result trees, actions, and externally configured rules. That is a much larger operational model than a deterministic code-level predicate. Reach for a rules engine when external rule management and workflow behavior are actual requirements, not simply because a conditional exists.
Why Use the Specification Pattern?
The pattern earns its place when naming and reuse improve the design more than another abstraction harms it.
One benefit is vocabulary. CustomerCanPlaceInvoiceOrder communicates intent at a call site in a way that a repeated compound condition may not. Another is focused testing. A named rule can be exercised at its important boundaries without constructing an entire application service. A third is controlled reuse. The same domain decision can be applied by multiple consumers without copying its details.
There are costs.
Every named rule adds a type, a file, a constructor, and another concept for readers to navigate. A codebase with dozens of one-line specifications can become harder to scan than a codebase with direct, local conditions. Generic base classes and composition helpers can also become a small framework that the team must understand and maintain.
Query-oriented specifications add a different cost. They can hide important query decisions if their API bundles filtering, ordering, loading, projection, and execution behind an innocent-looking name. The abstraction may make call sites shorter while making data access less visible.
The balanced conclusion is not "use specifications everywhere." It is "use them where a named criterion is a durable part of the design."
A Practical Use-or-Skip Decision
Consider a specification when several of these conditions are true:
- The condition has a domain or use-case name that matters to nontrivial code.
- The same rule is used by multiple consumers.
- The rule has meaningful positive, negative, and boundary cases.
- The condition changes independently from the workflows that consume it.
- Query criteria need a stable name and a reviewed provider-visible shape.
- Centralizing the criterion reduces genuine duplication rather than merely moving one line.
Prefer a direct conditional or inline LINQ when most of these are true:
- The condition is obvious and used once.
- It is tightly local to one method.
- Naming the rule adds no information.
- The extra type would be larger than the behavior it explains.
- The query is already readable and its shape should remain visible at the call site.
Consider a use-case-specific Query Object or query handler when the primary responsibility is producing a particular result shape rather than evaluating a reusable predicate. Consider a rules engine only when external configuration, workflow, actions, or rule lifecycle justify its operational cost.
The decision is contextual. Start with the simplest code that communicates the intent. Extract a specification when the rule's identity, reuse, or test surface becomes valuable.
How to Keep the Model Clear
Before introducing an interface, write one sentence that defines what a specification contains in your codebase.
For example:
A domain specification is an immutable, side-effect-free Boolean rule evaluated in memory.
Or:
A query specification contains provider-visible filtering criteria but does not own materialization.
Or:
A query envelope describes the complete read shape for one use case, including ordering and projection.
That sentence prevents accidental expansion. It also gives reviewers a standard for rejecting misplaced concerns. If your definition says "Boolean rule," then pagination does not belong there. If your definition says "complete read shape," then merge behavior and execution boundaries need explicit design.
Next, define the null policy. Decide whether a null candidate throws, returns false, or is impossible at the boundary. Then define mutability. Immutable specifications are easier to reason about than reusable objects whose criteria change after construction.
Finally, define proof. Pure domain rules need ordinary tests around their semantics. Provider-backed expressions need separate evidence that the provider can interpret them. Full query envelopes need review of every query-shaping field they own.
Frequently Asked Questions
Is the Specification Pattern only for DDD?
No. Its historical framing is closely associated with DDD, but the core idea is useful anywhere a named criterion improves a design. You do not need aggregates, repositories, or a full DDD architecture to use a pure domain specification.
Is a specification just a predicate?
A predicate is the Boolean behavior. A specification gives that behavior a meaningful identity and may add composition or metadata. If the name and reuse add no value, a plain predicate may be sufficient.
Should every specification use Expression<Func<T, bool>>?
No. Use an expression when a provider must inspect the criterion. A pure in-memory domain rule can expose IsSatisfiedBy. Choosing expressions for every rule can push provider constraints into code that does not need them.
Can a specification include ordering and pagination?
It can if your chosen model is a full query envelope. Those fields are not part of the historical predicate core, and they require explicit semantics. State that broader contract rather than presenting it as the only meaning of Specification.
When is the Specification Pattern overengineering?
It is probably overengineering when an obvious condition is local, used once, and easier to understand inline. The pattern becomes more compelling as rule identity, reuse, independent change, or focused testing becomes important.
The Core Decision
The Specification Pattern in C# is most useful when you are precise about what "specification" means.
A pure domain specification names a Boolean business rule. An expression specification exposes criteria for provider-backed querying. A full query envelope describes a broader read operation. Each model can be valid. Problems begin when one is described as another.
Use the smallest model that solves the actual problem. Keep domain rules free of persistence concerns. Keep provider-visible criteria honest about translation. Keep full query envelopes explicit about every piece of state they carry. That approach gives the pattern a clear job -- and gives your team a clear reason to keep it.

