A .NET library implementing the Specification pattern — small, composable, named rule objects that encapsulate query logic. Specifications (specs) work identically against in-memory collections (IEnumerable<T>) and (database) queries (IQueryable<T>), compose with &, |, and ! operators, and are well suited to be used with LINQ.
dotnet add package NSpecifications
Or via Package Manager Console:
Install-Package NSpecifications
A Spec<T> wraps a predicate expression and can be used directly anywhere a collection needs filtering:
var highRated = new Spec<Product>(p => p.Rating >= 4.5);
products.Where(highRated); // in-memory
dbContext.Products.Where(highRated).ToList(); // database — same codeSpecs can be defined inline or split into smaller named parts and combined with &, |, and !:
var electronics = new Spec<Product>(p => p.Category == ProductCategory.Electronics);
var affordable = new Spec<Product>(p => p.Price < 500m);
var affordableElectronics = electronics & affordable; // AND
var anyGoodDeal = electronics | affordable; // OR
var expensiveElectronics = electronics & !affordable; // NOTSpec<T> implicitly converts to both delegate types, so it can be passed anywhere they are expected.
var affordable = new Spec<Product>(p => p.Price < 500m);
repository.Find(affordable); // where Find expects Expression<Func<Product, bool>>
products.Where(affordable).ToList(); // works as Func<Product, bool> tooThe == and != operators conditionally negate a spec based on a bool value, which is useful for optional filters. spec == true and spec != false both return the spec as-is; spec == false and spec != true both return its negation.
// defined as a static member on the entity or in a dedicated specs class
static readonly Spec<Product> Available = new(p => p.IsAvailable);
// isAvailable = null → all products
// isAvailable = true → only available
// isAvailable = false → only unavailable
public Product[] FindProducts(bool? isAvailable = null)
{
var spec = Spec.Any<Product>();
if (isAvailable.HasValue)
spec = spec & (Available == isAvailable.Value);
return _repository.Find(spec);
}var inStock = new Spec<Product>(p => p.IsAvailable);
product.Is(inStock); // single object
new[] { product1, product2, product3 }.Are(inStock); // all must satisfyOperators (!, &, |, ==, !=) also work on any ISpecification<T> implementation via C# extension members.
ISpecification<Product> electronics = new Spec<Product>(p => p.Category == ProductCategory.Electronics);
ISpecification<Product> highRated = new Spec<Product>(p => p.Rating > 4.0);
var combined = electronics & highRated;Note:
ISpecification<T>operators returnISpecification<T>, which cannot be converted to anExpression<Func<T, bool>>. SeeSpec<T>vsISpecification<T>operators in the Technical Reference.
Specs are most useful when stored close to the entity they describe — as static members, in a dedicated ProductSpecs class, or in a shared Specs class. Here is a complete example:
public class Product
{
public string Name { get; }
public ProductCategory Category { get; }
public bool IsAvailable { get; }
public static readonly Spec<Product> Available = new(p => p.IsAvailable);
public static Spec<Product> InCategory(ProductCategory category) =>
new(p => p.Category == category);
}public IEnumerable<Product> FindProducts(ProductCategory? category = null, bool? isAvailable = null)
{
var spec = Spec.Any<Product>();
if (category.HasValue)
spec = spec & Product.InCategory(category.Value);
if (isAvailable.HasValue)
spec = spec & (Product.Available == isAvailable.Value);
return _repository.Find(spec);
}Specs defined as static readonly fields are instantiated once and reused. Specs that depend on a parameter are factory methods that create a new instance per call. Neither needs to be mocked in unit tests.
Spec<T> is a record that stores a predicate expression, making it implicitly convertible to Expression<Func<T, bool>> for use with IQueryable<T> providers (e.g. Entity Framework), and to Func<T, bool> for any method expecting a predicate delegate.
Operators on Spec<T> always return a new Spec<T>, so the stored expression is preserved through composition.
Naming tip: following Eric Evans' convention, name specs as objects rather than predicates — affordable, not isAffordable. A spec is more than a boolean; it's a reusable, composable rule.
For cases where implicit conversion to Expression<Func<T, bool>> is not needed, any class can implement ISpecification<T> directly:
public class InStockSpec : ISpecification<Product>
{
public bool IsSatisfiedBy(Product product) =>
product.IsAvailable && product.StockQuantity > 0;
}Composition via .And(), .Or(), .Not() extension methods and operators works the same way, but results are ISpecification<T> — suitable for in-memory validation only.
Both types support operators, but the return type is determined by the declared type of the operands:
| Left operand | Right operand | Result | Implicitly convertible? |
|---|---|---|---|
Spec<T> |
Spec<T> |
Spec<T> |
✅ Yes |
ISpecification<T> |
ISpecification<T> |
ISpecification<T> |
❌ No |
Spec<T> |
ISpecification<T> |
ISpecification<T> |
❌ No |
ISpecification<T> |
Spec<T> |
ISpecification<T> |
❌ No |
Spec<Product> available = new(p => p.IsAvailable);
Spec<Product> electronics = new(p => p.Category == ProductCategory.Electronics);
var availableElectronics = available & electronics; // Spec<T> ✅
dbContext.Products.Where(availableElectronics).ToList();
ISpecification<Product> custom = new MyCustomSpec();
var availableAndCustom = available & custom; // ISpecification<T> ❌
dbContext.Products.Where(availableAndCustom); // won't compile
inMemoryList.Where(availableAndCustom.IsSatisfiedBy); // ✅ in-memory onlyRule of thumb: keep all operands as Spec<T> when database queries are involved.
Spec.Any<Product>() // always satisfied — also Spec<Product>.Any
Spec.None<Product>() // never satisfied — also Spec<Product>.None
Spec.Create<Product>(p => p.IsAvailable) // explicit factory — also Spec<Product>.Create(...)