BrandGhost
Specification vs Repository vs Query Object in .NET

Specification vs Repository vs Query Object in .NET

Specification vs Repository is not a contest where one pattern replaces the others. The terms describe different responsibilities, and confusion starts when a single class is expected to name a business rule, construct a database query, hide persistence, validate input, and decide authorization. This comparison keeps those jobs separate so you can recognize the smallest abstraction that fits.

The short version is simple. A Specification names criteria. A Repository mediates access to persisted domain objects. A Query Object represents a database query. They can collaborate, but they do not answer the same question.

Start With the Question Each Abstraction Answers

The historical Specification described by Eric Evans and Martin Fowler is an encapsulated predicate. It answers whether a candidate satisfies a condition. That condition might support selection, validation-adjacent checks, or describing what needs to be built. The important part is the named criterion.

Fowler's Repository description gives Repository a different job. It mediates between the domain and data-mapping layers through a collection-like interface. A client can submit criteria, including a Specification, but the Repository owns the persistence-facing access boundary.

Fowler's Query Object is narrower in another direction. It represents a database query and translates object-oriented criteria into a query language. Its reason to exist is retrieval, not general domain truth.

Here is the first decision table:

Abstraction Primary question Typical output Natural home
Specification Does this candidate satisfy named criteria? bool, predicate, or criteria expression Domain or application code
Repository How does the application access persisted objects through a controlled boundary? Entities, projections, counts, or operation results Infrastructure behind an application-facing interface
Query Object What database query represents this use case? A projected result, page, scalar, or query model Application or data-access code

These descriptions are intentionally bounded. Modern libraries sometimes call a full query envelope a "specification." Such an object may contain criteria, ordering, includes, projection, pagination, tracking flags, and tags. That is valid package terminology, but architecturally it behaves much more like a named query description than the historical predicate-only pattern.

Small Interface Sketches Make the Boundary Visible

You do not need a framework to see the difference. These interfaces are sketches, not implementation prescriptions:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

namespace ComparisonSketches;

public interface IDomainSpecification<in T>
{
    bool IsSatisfiedBy(T candidate);
}

public interface IExpressionSpecification<T>
{
    Expression<Func<T, bool>> Criteria { get; }
}

public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken);

    Task<IReadOnlyList<T>> ListAsync(
        IExpressionSpecification<T> specification,
        CancellationToken cancellationToken);
}

The Specification carries criteria. The Repository accepts criteria and controls execution. Neither interface says that every query must use a Repository, nor that a Specification must know how data is stored.

A Query Object can instead own one use-case-shaped result:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace ComparisonSketches;

public interface IQueryObject<TResult>
{
    Task<TResult> ExecuteAsync(
        CancellationToken cancellationToken);
}

public sealed record CustomerSummary(
    Guid Id,
    string DisplayName);

public sealed record CustomerSummaryPage(
    IReadOnlyList<CustomerSummary> Items,
    int TotalCount);

This shape makes a different tradeoff. The query contract directly expresses the result needed by the caller. It does not pretend that every retrieval operation is a collection operation over domain entities.

Specification vs Repository

The Specification vs Repository distinction becomes clear when you separate criteria from execution.

A pure Specification can be evaluated against an in-memory object. An expression-based Specification can expose provider-visible criteria. In both cases, its central meaning is "these conditions define a match."

A Repository coordinates access to persisted objects. It may apply a Specification, add mandatory tenant constraints, materialize results, save changes, or expose use-case-specific methods. The Repository boundary is also where you can prevent callers from depending on provider-specific IQueryable<T> behavior.

Choose this emphasis Benefits Costs and risks
Specification without Repository Keeps a named rule reusable and avoids adding a persistence facade when direct data access is already appropriate Execution policy remains with the caller or another service
Repository accepting Specifications Centralizes persistence access and keeps provider execution behind an interface Can become a generic catch-all with weak, overly broad methods
Repository with explicit use-case methods Makes important operations discoverable and constrains query behavior More methods and more interface maintenance

Repository does not become unnecessary just because a Specification exists. Specification also does not become mandatory because a Repository exists. The combination is useful when several callers need named criteria while persistence execution must remain controlled.

Be cautious with a generic method such as ListAsync(ISpecification<T>) if the application needs many unrelated projections and result shapes. The method may accumulate hidden conventions around includes, paging, tracking, and authorization. At that point, a use-case-specific Query Object can communicate more clearly.

Specification vs Query Object

A Specification and a Query Object overlap when both contain filter criteria. Their intent is still different.

A Specification is strongest when the criterion has a stable name in the problem domain: an account is eligible for renewal, an order requires review, or a candidate matches a search boundary. That rule may be reused in multiple decisions.

A Query Object is strongest when the retrieval operation is the concept: load an account dashboard, return a paged support queue, or calculate a monthly summary. Projection, sorting, grouping, pagination, and database-specific choices are often part of that use case.

Design signal Specification Query Object
Named Boolean condition is the main concept Strong fit Possible but broader than needed
Exact result shape is the main concept Limited fit Strong fit
Reuse in an in-memory domain decision Strong fit Weak fit
Joins, grouping, projection, and paging define the operation A full query envelope can carry them, but meaning becomes broader Natural fit
Caller should receive one purpose-built result Possible through projection Natural fit

A modern query specification can reasonably be described as a specialized Query Object. The label matters less than an honest contract. If the object carries query-shaping state, document it as a query description. Do not market it as a pure business rule simply because it has "Specification" in the class name.

A Predicate Is Not Automatically a Specification

.NET defines Predicate<T> as a delegate that returns a Boolean value for an object. A lambda such as customer => customer.IsActive is therefore a predicate.

A Specification adds identity and intent around a predicate. ActiveCustomerSpecification gives the condition a name, a reusable type, and a place for domain-focused tests. It may also support composition or descriptive metadata.

That additional structure is not automatically valuable. If a predicate appears once, is obvious in context, and has no independent business meaning, an inline lambda can be clearer. Creating a class for every Where clause produces ceremony without necessarily improving the design.

Use a Specification when naming and reuse earn their keep. Use a plain predicate when the condition is local and uncomplicated.

Specification Is Not a Gang of Four Pattern

This point needs a direct answer: Specification is not one of the Gang of Four's 23 design patterns.

The publisher's page for Design Patterns: Elements of Reusable Object-Oriented Software describes the 1994 catalog of creational, structural, and behavioral patterns. Specification is not in that catalog. Its well-known lineage comes from domain-driven design and enterprise application design, including the Evans/Fowler material.

Why does the confusion persist? A specification implementation can use GoF patterns internally:

  • Composite can represent trees of AND, OR, and NOT criteria.
  • Strategy can swap the algorithm used to evaluate or translate criteria.

Using a GoF pattern to implement part of another design does not add that design to the GoF catalog. If you want broader context for the original catalog, the Big List of Design Patterns provides a useful map.

Specification vs Strategy and Composite

Strategy and Composite deserve bounded comparisons because their structures can appear in Specification code.

Strategy answers how an operation should be performed. A tax calculation strategy, compression strategy, or routing strategy provides an interchangeable algorithm. Specification answers whether criteria are satisfied.

Composite organizes individual objects and groups through a uniform interface. An AndSpecification can be a composite node containing child specifications. Composite explains that tree structure. Specification explains the predicate meaning.

Pattern Core intent Example question
Specification Express named criteria Is this order eligible for automatic approval?
Strategy Select interchangeable behavior Which approval algorithm should process this order?
Composite Treat leaves and groups uniformly Can one evaluator traverse atomic and combined criteria the same way?

These patterns can collaborate. They should not be treated as synonyms.

Validation Is a Neighbor, Not a Replacement Name

The historical Specification literature includes validation as a use. That does not mean a Boolean Specification is a complete validation system.

A Boolean rule can answer, "Is the credit limit sufficient?" Real validation often needs more:

  • A stable error code.
  • A human-readable message.
  • The field or domain member involved.
  • Multiple failures in one response.
  • Boundary-specific behavior for malformed input.

Microsoft's domain-model validation guidance keeps aggregate invariants in the domain model and discusses Specification plus Notification as an advanced option. That is a useful separation. Specifications can provide reusable rule predicates, while a notification or result adapter provides structured failures. The aggregate still enforces valid state transitions.

Request validation is another boundary. It answers whether incoming data is present, well-formed, and acceptable to process. A domain Specification should not be forced to parse transport formats or reproduce model-binding concerns.

Authorization Policy Is a Security Decision

A Specification may filter records that appear relevant to a user, but it is not an authorization policy.

ASP.NET Core policy-based authorization evaluates requirements through handlers against a user and, when needed, a resource. It owns security semantics such as authenticated identity, claims, requirements, success, and failure.

A data criterion such as document => document.OwnerId == userId can support row selection. It does not, by itself, prove that the current principal is allowed to read, modify, export, or disclose the document. Authorization must remain an independently enforced boundary.

This distinction is not academic. Query criteria can be omitted, combined incorrectly, or bypassed. Security decisions need explicit policy enforcement and tests that do not depend on a convenient Repository or Specification call happening first.

Rules Engines Solve a Larger Problem

A Specification normally represents deterministic criteria in code. A rules engine addresses a broader operational model.

The official Microsoft RulesEngine documentation describes workflows, multiple inputs, scoped parameters, result trees, actions, and rules supplied through external configuration. Those capabilities support scenarios where rules are authored, stored, grouped, evaluated, and acted upon through a runtime system.

Need Specification Rules engine
Small named predicate in code Strong fit Usually excessive
Boolean reuse in domain decisions Strong fit Possible
External rule configuration Not inherent Strong fit
Workflow and action execution Keep separate Supported by engine model
Rich result tree across many rules Requires a separate adapter Natural capability
Operational governance for changing rules Application responsibility Often part of the platform design

Do not move to a rules engine merely because several Specifications exist. The extra runtime, configuration, observability, and governance concerns are real. Conversely, do not stretch a set of Boolean objects into a home-grown rules platform when external authorship and workflow execution are actual requirements.

A Practical Selection Matrix

The most useful Specification vs Repository decision starts with the dominant responsibility:

Situation Prefer Why
One local, readable filter Inline predicate The name and class add little value
Reusable domain condition Pure Specification The rule gains identity without persistence coupling
Reusable provider-visible criterion Expression Specification The provider can inspect the expression
Controlled persistence boundary over domain objects Repository Access and materialization stay behind a collection-like contract
Use-case-specific projection, grouping, or page Query Object The operation and result shape are explicit
Structured domain-rule failures Specification plus result adapter Boolean rule and failure reporting remain separate
User/resource access decision Authorization policy Security semantics belong to requirements and handlers
Externally configured workflows and actions Rules engine The problem exceeds a code-level predicate

There is no universal winner. The tradeoff is clarity against ceremony. Each abstraction should remove ambiguity from the responsibility it owns. If it merely moves a short expression into another file, it has not earned much.

Frequently Asked Questions

Can a Repository execute a Specification?

Yes. Fowler's Repository description explicitly allows declarative query specifications to be submitted to a Repository. The Specification supplies criteria, while the Repository controls persistence access and execution.

Can I use Specifications without a Repository?

Yes. A pure Specification can run in memory, and an expression Specification can be applied directly by application or data-access code. Repository is an independent architectural choice.

Is a query specification just a Query Object?

Often it is a specialized form of Query Object, especially when it carries ordering, projection, includes, and pagination. A predicate-only domain Specification is narrower and has a different center of gravity.

Is Specification a behavioral GoF pattern?

No. Specification is not one of the GoF 23. It may use Strategy or Composite internally, but that implementation relationship does not change its historical catalog.

Should validation return only true or false?

Not when callers need actionable failures. Keep the reusable Boolean rule in a Specification if it helps, then adapt failed rules into structured codes and messages. Request validation and aggregate invariant enforcement remain separate.

Can a Specification enforce authorization?

It can contribute criteria, but it should not be the authorization boundary. Use authorization policies and handlers to evaluate the current user, requirements, and resource access.

Choose by Responsibility, Not by Pattern Count

For Specification vs Repository, the clean decision is not "which pattern is better?" Ask what needs a name, what needs execution, and what needs a boundary.

Use a Specification for meaningful criteria. Use a Repository when persistence access needs mediation. Use a Query Object when the retrieval operation and result shape are the concept. Keep validation, invariants, authorization, and rules-engine concerns in their own contracts.

That approach may use fewer patterns, or it may use several together. Either outcome is fine. The goal is a design where each abstraction tells the truth about the responsibility it owns.

Specification Pattern With EF Core Without a Repository

Use the Specification Pattern with EF Core DbContext queries while keeping tracking, result limits, cancellation, SQL checks, and relational tests explicit.

The Specification Design Pattern in C#: What You Need To Know

Learn about the Specification Design Pattern in C# and its benefits for your code. See how this pattern can improve code quality and how to implement it!

Specification Pattern in C#: A Practical Guide for Modern .NET

Understand the Specification Pattern in C#, distinguish domain rules from query criteria and query envelopes, and decide when it fits a modern .NET design.

An error has occurred. This application may no longer respond until reloaded. Reload