A lightweight C# implementation of the Specification pattern that lets you encapsulate business rules as reusable, composable objects and combine them fluently with And, Or, and Not.
Instead of scattering if conditions and LINQ predicates across your codebase, you express each rule as a named class (or a one-liner lambda). Rules stay decoupled from your domain objects and can be composed without touching either.
| Class | Purpose |
|---|---|
SpecificationBase<T> |
Abstract base – implement SatisfyingElementsFrom to define your rule |
CompositeSpecification<T> |
Extends SpecificationBase<T> with And, Or, and Not combinators |
FunctionalSpecification<T> |
Ready-made specification built from a lambda – no subclass needed |
Subclass CompositeSpecification<T> and express the rule in LINQ:
public class OverDueLoanSpecification : CompositeSpecification<Loan>
{
public override IQueryable<Loan> SatisfyingElementsFrom(IQueryable<Loan> candidates)
{
return from l in candidates
where l.DateTaken < DateTime.Today
where l.DateTaken.Add(l.LoanPeriod) < DateTime.Now
select l;
}
}var customer = new Customer { FirstName = "Alice", Balance = 500.00 };
var valuedCustomer = new ValuedCustomerSpecification(); // Balance > 1300
bool isValued = valuedCustomer.IsSatisfiedBy(customer); // falsevar overdueSpec = new OverDueLoanSpecification();
IQueryable<Loan> overdueLoans = overdueSpec.SatisfyingElementsFrom(loans.AsQueryable());When the rule is simple enough that a dedicated class would be overkill, use FunctionalSpecification<T>:
var bigLoanSpec = new FunctionalSpecification<Loan>(l => l.Amount > 10_000.00);
IQueryable<Loan> bigLoans = bigLoanSpec.SatisfyingElementsFrom(loans.AsQueryable());Specifications that extend CompositeSpecification<T> (including FunctionalSpecification<T>) expose And, Or, and Not:
var overdueSpec = new OverDueLoanSpecification();
var bigLoanSpec = new FunctionalSpecification<Loan>(l => l.Amount > 10_000.00);
var sixDaysSpec = new FunctionalSpecification<Loan>(l => l.LoanPeriod == TimeSpan.FromDays(6));
var fortDaysSpec = new FunctionalSpecification<Loan>(l => l.LoanPeriod == TimeSpan.FromDays(14));
// AND – overdue AND large
var overdueAndBig = overdueSpec.And(bigLoanSpec);
// OR – 6-day or 14-day loans
var sixOrFourteen = sixDaysSpec.Or(fortDaysSpec);
// NOT – anything that is NOT a 6-day loan
var notSixDays = sixDaysSpec.Not();
// Use just like any other specification
IQueryable<Loan> results = repo.FindElementsBy(overdueAndBig);Combinators can be chained arbitrarily:
var complex = overdueSpec.And(bigLoanSpec).And(sixDaysSpec.Not());Pass a specification into your repository to keep query logic out of your data layer:
public class LoanRepository
{
public IQueryable<Loan> FindElementsBy(SpecificationBase<Loan> spec)
{
// fetch your data source, then delegate filtering to the spec
return spec.SatisfyingElementsFrom(loans.AsQueryable());
}
}
// Caller decides the rule – repository stays unchanged
var results = repo.FindElementsBy(overdueSpec.And(bigLoanSpec));FluentSpecification/
├── FluentSpecification/ # Core library
│ ├── SpecificationBase.cs
│ ├── CompositeSpecification.cs
│ ├── AndSpecification.cs
│ ├── OrSpecification.cs
│ ├── NotSpecification.cs
│ └── FunctionalSpecification.cs
└── FluentSpecification.Samples/ # Runnable examples (Loan / Customer domain)