Skip to main content
Version: Next

Specifications

Overview

The Specification pattern encapsulates a query predicate as a named, reusable object. Instead of scattering raw lambda expressions throughout your handlers and services, you define a specification class once and reference it by name. This keeps query logic close to the domain, makes intent explicit, and allows query logic to be tested independently of any database.

RCommon ships two interfaces and two concrete implementations:

  • ISpecification<T> — a single filter predicate
  • IPagedSpecification<T> — adds ordering and paging on top of a filter
  • Specification<T> — default implementation, supports & and | operator composition
  • PagedSpecification<T> — default implementation of IPagedSpecification<T>

All repository interfaces accept specifications alongside their equivalent lambda-based overloads, so you can switch between the two styles freely.

Installation

Specifications are part of the core package:

NuGet Package
dotnet add package RCommon.Persistence

Defining a Specification

Inherit from Specification<T> and pass the filter predicate to the base constructor:

using RCommon;

public class AllocationExistsSpec : Specification<LeaveAllocation>
{
public AllocationExistsSpec(string userId, int leaveTypeId, int period)
: base(q => q.EmployeeId == userId
&& q.LeaveTypeId == leaveTypeId
&& q.Period == period)
{
}
}

This is a real specification from the CleanWithCQRS example. It encapsulates a three-part uniqueness check for a leave allocation.

Using a Specification with a Repository

Pass the specification to any repository method that accepts ISpecification<TEntity>:

var spec = new AllocationExistsSpec(employeeId, leaveTypeId, currentYear);

long count = await _allocationRepository.GetCountAsync(spec, cancellationToken);

if (count == 0)
{
await _allocationRepository.AddAsync(newAllocation, cancellationToken);
}

Find a collection:

ICollection<LeaveAllocation> matches = await _allocationRepository
.FindAsync(spec, cancellationToken);

Find single or default:

LeaveAllocation? existing = await _allocationRepository
.FindSingleOrDefaultAsync(spec, cancellationToken);

Check existence:

bool exists = await _allocationRepository.AnyAsync(spec, cancellationToken);

Combining Specifications

Specification<T> overloads the & and | operators to compose predicates without additional subclassing:

var activeSpec  = new Specification<Order>(o => o.IsActive);
var pendingSpec = new Specification<Order>(o => o.Status == OrderStatus.Pending);
var largeSpec = new Specification<Order>(o => o.Total > 1000m);

// Both conditions must be true
var activePending = activeSpec & pendingSpec;

// Either condition is sufficient
var activeOrLarge = activeSpec | largeSpec;

The same composition is available through extension methods on ISpecification<T> when you hold an interface reference:

ISpecification<Order> combined = activeSpec
.And(pendingSpec)
.And(largeSpec);

Negate a specification:

ISpecification<Order> notActive = activeSpec.Not();

Paged Specifications

When you need filtering, ordering, and paging in a single object, use PagedSpecification<T>:

var pagedSpec = new PagedSpecification<Order>(
predicate: o => o.CustomerId == customerId,
orderByExpression: o => o.DateCreated,
orderByAscending: false,
pageNumber: 1,
pageSize: 25);

IPaginatedList<Order> page = await _orderRepository
.FindAsync(pagedSpec, cancellationToken);

IPagedSpecification<T> exposes:

PropertyTypeDescription
PredicateExpression<Func<T, bool>>The filter expression
OrderByExpressionExpression<Func<T, object>>The ordering expression
OrderByAscendingboolSort direction
PageNumberint1-based page number
PageSizeintNumber of items per page

Direct Predicate Evaluation

Specifications can be evaluated in memory against a single entity, which is useful in unit tests or domain validation:

var spec = new AllocationExistsSpec(userId, leaveTypeId, year);
bool satisfied = spec.IsSatisfiedBy(allocation);

API Summary

TypePurpose
ISpecification<T>Contract: Expression<Func<T, bool>> Predicate and bool IsSatisfiedBy(T entity)
IPagedSpecification<T>Extends ISpecification<T> with PageNumber, PageSize, OrderByExpression, OrderByAscending
Specification<T>Concrete implementation; supports & and `
PagedSpecification<T>Concrete implementation of IPagedSpecification<T>
SpecificationExtensionsExtension methods: .And(), .Or(), .Not() on ISpecification<T>
RCommonRCommon