Myth.Specification
4.3.0
dotnet add package Myth.Specification --version 4.3.0
NuGet\Install-Package Myth.Specification -Version 4.3.0
<PackageReference Include="Myth.Specification" Version="4.3.0" />
<PackageVersion Include="Myth.Specification" Version="4.3.0" />
<PackageReference Include="Myth.Specification" />
paket add Myth.Specification --version 4.3.0
#r "nuget: Myth.Specification, 4.3.0"
#:package Myth.Specification@4.3.0
#addin nuget:?package=Myth.Specification&version=4.3.0
#tool nuget:?package=Myth.Specification&version=4.3.0
Myth.Specification
Implementation of the Specification Pattern for building reusable, composable, and testable query logic in .NET applications.
🎯 Why Myth.Specification?
Query logic scattered in repositories kills maintainability. Duplicate WHERE clauses everywhere, business rules buried in SQL, impossible to reuse queries, can't test query logic independently. Myth.Specification encapsulates business rules as reusable, composable, testable objects that work with EF Core, LINQ, and in-memory collections. Build complex queries by combining simple specifications. Test query logic without database. Change queries without touching repositories.
The Problem: Query Logic Duplication
Same filtering logic duplicated across 10 repositories. Business rules ("active products") buried in SQL. Can't reuse or test queries. Changing criteria breaks app everywhere.
The Solution: Specification Pattern
Encapsulate queries: ActiveProducts, ExpensiveProducts as specifications. Compose: Combine with .And(), .Or(), .Not(). Reuse: Same specification in repository, controller, validation. Test: In-memory .IsSatisfiedBy() validates without database. Fluent: .Order(), .Take(), .Skip() for complete queries.
Key Benefits
Reusable: Write query once, use everywhere. Testable: Validate specifications in-memory without DB. Composable: Combine simple specs into complex queries. DDD-aligned: Specifications are DDD tactical pattern for business rules. Type-safe: Expression-based, compile-time checked.
Real-World Applications
E-Commerce: ActiveProducts(), InPriceRange(min, max), InCategory(cat) composed for product search. Reports: Complex filtering as specifications, reused across multiple reports. Authorization: User permissions as specifications applied to queries.
Features
- Composable Specifications - Combine specifications with AND, OR, NOT logical operators
- Conditional Composition - AndIf/OrIf for conditional specification building
- Ordering Support - Ascending and descending ordering with multi-level ThenBy support
- Pagination - Built-in Skip, Take, and WithPagination methods
- Distinct Operations - DistinctBy for unique results by property
- Expression-Based - Full support for
Expression<Func<T, bool>>and IQueryable - Fluent API - Chainable methods for readable query building
- In-Memory Validation - IsSatisfiedBy for entity validation
- Extension Methods - Seamless integration with IEnumerable and IQueryable
Installation
dotnet add package Myth.Specification
Quick Start
Basic Specification
using Myth.Interfaces;
using Myth.Specifications;
// Create a specification
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.And(p => p.Price > 100)
.Order(p => p.Name)
.Take(10);
// Apply to query
var products = await dbContext.Products
.Where(spec.Predicate)
.OrderBy(spec.Sort)
.Take(spec.ItemsTaked)
.ToListAsync();
// Or use extension method
var products = await dbContext.Products
.Specify(spec)
.ToListAsync();
Reusable Specification Extensions
public static class ProductSpecifications {
public static ISpec<Product> IsActive(this ISpec<Product> spec) {
return spec.And(p => p.IsActive);
}
public static ISpec<Product> InCategory(this ISpec<Product> spec, string category) {
return spec.And(p => p.Category == category);
}
public static ISpec<Product> PriceRange(this ISpec<Product> spec, decimal min, decimal max) {
return spec.And(p => p.Price >= min && p.Price <= max);
}
public static ISpec<Product> OrderByPrice(this ISpec<Product> spec, bool descending = false) {
return descending
? spec.OrderDescending(p => p.Price)
: spec.Order(p => p.Price);
}
}
// Usage
var spec = SpecBuilder<Product>.Create()
.IsActive()
.InCategory("Electronics")
.PriceRange(100, 1000)
.OrderByPrice(descending: true)
.Take(20);
var products = dbContext.Products.Specify(spec).ToList();
ISpec<T> Interface
The core interface for all specifications.
Properties
Expression<Func<T, bool>> Predicate { get; } // Filter expression
Func<T, bool> Query { get; } // Compiled predicate
Func<IQueryable<T>, IOrderedQueryable<T>> Sort { get; } // Ordering function
Func<IQueryable<T>, IQueryable<T>> PostProcess { get; } // Post-processing (Skip/Take/Distinct)
int ItemsSkiped { get; } // Number of items to skip
int ItemsTaked { get; } // Number of items to take
Methods
Logical Operations
ISpec<T> And(ISpec<T> specification)
ISpec<T> And(Expression<Func<T, bool>> expression)
ISpec<T> AndIf(bool condition, ISpec<T> other)
ISpec<T> AndIf(bool condition, Expression<Func<T, bool>> other)
ISpec<T> Or(ISpec<T> specification)
ISpec<T> Or(Expression<Func<T, bool>> expression)
ISpec<T> OrIf(bool condition, ISpec<T> other)
ISpec<T> OrIf(bool condition, Expression<Func<T, bool>> other)
ISpec<T> Not()
Ordering
ISpec<T> Order<TProperty>(Expression<Func<T, TProperty>> property)
ISpec<T> OrderDescending<TProperty>(Expression<Func<T, TProperty>> property)
Pagination
ISpec<T> Skip(int amount)
ISpec<T> Take(int amount)
ISpec<T> WithPagination(Pagination pagination) // Uses Myth.Commons Pagination value object
Distinct
ISpec<T> DistinctBy<TProperty>(Expression<Func<T, TProperty>> property)
Query Execution
IQueryable<T> Prepare(IQueryable<T> query) // Apply filter + sort + post-process
IQueryable<T> Filtered(IQueryable<T> query) // Apply only filter
IQueryable<T> Sorted(IQueryable<T> query) // Apply only sorting
IQueryable<T> Processed(IQueryable<T> query) // Apply only post-processing
T? SatisfyingItemFrom(IQueryable<T> query) // Get first matching item
IQueryable<T> SatisfyingItemsFrom(IQueryable<T> query) // Get all matching items
Validation
bool IsSatisfiedBy(T entity) // Check if entity satisfies specification (in-memory)
Initialization
ISpec<T> InitEmpty() // Reset to empty specification
SpecBuilder<T>
Abstract base class for creating specifications with a fluent API.
Creating Specifications
// Start with empty specification
var spec = SpecBuilder<Product>.Create();
// Chain operations
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.And(p => p.Price > 0)
.Order(p => p.Name)
.Take(10);
Implicit Conversion
SpecBuilder<T> → Expression<Func<T, bool>>
// Can be used directly where Expression is expected
Expression<Func<Product, bool>> expr = SpecBuilder<Product>.Create()
.And(p => p.IsActive);
Logical Operations
AND
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.And(p => p.Price > 100)
.And(p => p.Stock > 0);
// Conditional AND
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.AndIf(!string.IsNullOrEmpty(searchTerm), p => p.Name.Contains(searchTerm))
.AndIf(minPrice.HasValue, p => p.Price >= minPrice.Value);
OR
var spec = SpecBuilder<Product>.Create()
.Or(p => p.Category == "Electronics")
.Or(p => p.Category == "Computers");
// Conditional OR
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.OrIf(includeDiscounted, p => p.IsDiscounted);
NOT
var activeSpec = SpecBuilder<Product>.Create()
.And(p => p.IsActive);
var inactiveSpec = activeSpec.Not(); // Inverts the specification
Complex Combinations
// (IsActive AND Price > 100) OR (IsDiscounted AND Stock > 0)
var spec1 = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.And(p => p.Price > 100);
var spec2 = SpecBuilder<Product>.Create()
.And(p => p.IsDiscounted)
.And(p => p.Stock > 0);
var combinedSpec = spec1.Or(spec2);
Ordering
Single Level
// Ascending
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.Order(p => p.Name);
// Descending
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.OrderDescending(p => p.Price);
Multi-Level Ordering
The specification automatically uses ThenBy/ThenByDescending for subsequent ordering:
var spec = SpecBuilder<Product>.Create()
.Order(p => p.Category) // OrderBy
.OrderDescending(p => p.Price) // ThenByDescending
.Order(p => p.Name); // ThenBy
// Equivalent to:
query.OrderBy(p => p.Category)
.ThenByDescending(p => p.Price)
.ThenBy(p => p.Name);
Pagination
Skip and Take
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.Skip(20) // Skip first 20 items
.Take(10); // Take next 10 items
Console.WriteLine($"Skipped: {spec.ItemsSkiped}"); // 20
Console.WriteLine($"Taken: {spec.ItemsTaked}"); // 10
WithPagination
Uses the Pagination value object from Myth.Commons:
var pagination = new Pagination(pageNumber: 2, pageSize: 20);
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.WithPagination(pagination);
// Automatically calculates: Skip((2-1) * 20) = Skip(20), Take(20)
Special Pagination Values
// Get all items (no pagination)
var spec = SpecBuilder<Product>.Create()
.WithPagination(Pagination.All);
// Default pagination (page 1, size 10)
var spec = SpecBuilder<Product>.Create()
.WithPagination(Pagination.Default);
Distinct Operations
// Get distinct products by brand
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.DistinctBy(p => p.Brand);
// Get distinct products by category and brand combination
var spec = SpecBuilder<Product>.Create()
.DistinctBy(p => new { p.Category, p.Brand });
Extension Methods
IQueryable<T> Extensions
// Apply filter only
IQueryable<T> Filter<T>(this IQueryable<T> values, ISpec<T> spec)
// Apply sort only
IQueryable<T> Sort<T>(this IQueryable<T> values, ISpec<T> spec)
// Apply pagination only
IQueryable<T> Paginate<T>(this IQueryable<T> values, ISpec<T> spec)
IEnumerable<T> Extensions
// Filter using compiled query
IEnumerable<T> Where<T>(this IEnumerable<T> values, ISpec<T> spec)
// Apply complete specification (convert to IQueryable first)
IQueryable<T> Specify<T>(this IEnumerable<T> values, ISpec<T> spec)
Usage Examples
var products = dbContext.Products;
// Apply only filter
var filtered = products.Filter(spec);
// Apply only sorting
var sorted = products.Sort(spec);
// Apply only pagination
var paginated = products.Paginate(spec);
// Apply complete specification
var result = products.Specify(spec);
// In-memory filtering
var memoryList = new List<Product> { /* ... */ };
var filtered = memoryList.Where(spec); // Uses compiled predicate
Query Execution Methods
Prepare
Applies filter + sort + post-processing in sequence:
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.Order(p => p.Name)
.Take(10);
var query = spec.Prepare(dbContext.Products);
// Equivalent to:
// dbContext.Products
// .Where(p => p.IsActive)
// .OrderBy(p => p.Name)
// .Take(10)
Filtered
Applies only the filter predicate:
var filtered = spec.Filtered(dbContext.Products);
// Only applies: .Where(predicate)
Sorted
Applies only the sorting function:
var sorted = spec.Sorted(dbContext.Products);
// Only applies: .OrderBy(...).ThenBy(...)
Processed
Applies only post-processing (Skip/Take/Distinct):
var processed = spec.Processed(dbContext.Products);
// Only applies: .Skip(...).Take(...).DistinctBy(...)
SatisfyingItemFrom
Gets the first item that satisfies the specification:
var product = spec.SatisfyingItemFrom(dbContext.Products);
// Equivalent to: Prepare(query).FirstOrDefault()
SatisfyingItemsFrom
Gets all items that satisfy the specification:
var products = spec.SatisfyingItemsFrom(dbContext.Products);
// Equivalent to: Prepare(query)
In-Memory Validation
IsSatisfiedBy
Check if an entity satisfies the specification without database query:
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.And(p => p.Price > 100);
var product = new Product { IsActive = true, Price = 150 };
if (spec.IsSatisfiedBy(product)) {
Console.WriteLine("Product matches specification");
}
Note: Throws InvalidSpecificationException if Predicate is null.
Complete Example
// Define reusable specifications
public static class OrderSpecifications {
public static ISpec<Order> ForCustomer(this ISpec<Order> spec, Guid customerId) {
return spec.And(o => o.CustomerId == customerId);
}
public static ISpec<Order> WithStatus(this ISpec<Order> spec, params OrderStatus[] statuses) {
return spec.And(o => statuses.Contains(o.Status));
}
public static ISpec<Order> CreatedAfter(this ISpec<Order> spec, DateTime date) {
return spec.And(o => o.CreatedAt >= date);
}
public static ISpec<Order> MinimumAmount(this ISpec<Order> spec, decimal amount) {
return spec.And(o => o.TotalAmount >= amount);
}
public static ISpec<Order> RecentFirst(this ISpec<Order> spec) {
return spec.OrderDescending(o => o.CreatedAt);
}
public static ISpec<Order> SearchByNumber(this ISpec<Order> spec, string orderNumber) {
return spec.AndIf(
!string.IsNullOrEmpty(orderNumber),
o => o.OrderNumber.Contains(orderNumber));
}
}
// Repository method
public class OrderRepository : IOrderRepository {
private readonly DbContext _context;
public async Task<IPaginated<Order>> SearchOrders(OrderSearchDto search) {
var spec = SpecBuilder<Order>.Create()
.ForCustomer(search.CustomerId)
.WithStatus(OrderStatus.Pending, OrderStatus.Processing)
.CreatedAfter(DateTime.UtcNow.AddMonths(-3))
.MinimumAmount(search.MinAmount ?? 0)
.SearchByNumber(search.OrderNumber)
.RecentFirst()
.WithPagination(search.Pagination);
var orders = await _context.Orders
.Specify(spec)
.ToListAsync();
var totalCount = await _context.Orders
.Filter(spec)
.CountAsync();
return new Paginated<Order>(
pageNumber: search.Pagination.PageNumber,
pageSize: search.Pagination.PageSize,
totalItems: totalCount,
totalPages: (int)Math.Ceiling((double)totalCount / search.Pagination.PageSize),
items: orders);
}
public async Task<bool> HasPendingOrders(Guid customerId) {
var spec = SpecBuilder<Order>.Create()
.ForCustomer(customerId)
.WithStatus(OrderStatus.Pending);
return await _context.Orders
.Filter(spec)
.AnyAsync();
}
}
// Controller
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase {
private readonly IOrderRepository _repository;
[HttpGet]
public async Task<IActionResult> Search([FromQuery] OrderSearchDto search) {
var result = await _repository.SearchOrders(search);
return Ok(result);
}
[HttpGet("validate/{id}")]
public async Task<IActionResult> Validate(Guid id) {
var order = await _repository.GetByIdAsync(id);
if (order == null)
return NotFound();
var validSpec = SpecBuilder<Order>.Create()
.WithStatus(OrderStatus.Pending, OrderStatus.Processing)
.MinimumAmount(10);
if (validSpec.IsSatisfiedBy(order)) {
return Ok(new { message = "Order is valid for processing" });
}
return BadRequest(new { message = "Order does not meet requirements" });
}
}
Exceptions
InvalidSpecificationException
Thrown when Predicate is null in IsSatisfiedBy():
[Serializable]
public sealed class InvalidSpecificationException : Exception
SpecificationException
Thrown when errors occur during specification execution:
public class SpecificationException : Exception
Thrown by:
Filtered()- Error applying filterSorted()- Error applying sortProcessed()- Error applying post-processing
Best Practices
- Create Extension Methods - Define reusable specifications as extension methods on
ISpec<T> - Keep Specifications Simple - Each specification should represent a single business rule
- Use Conditional Composition - Use
AndIf/OrIffor optional filters - Separate Concerns - Keep query logic separate from repository implementation
- Test Specifications - Use
IsSatisfiedBy()for unit testing specifications - Use Pagination Value Objects - Leverage
Myth.Commons.Paginationfor consistent paging - Name Meaningfully - Use descriptive names like
IsActive(),RecentFirst() - Avoid Complex Expressions - Break complex filters into multiple specifications
Integration with Myth.Repository
When used with Myth.Repository.EntityFramework:
public interface IProductRepository : IReadRepositoryAsync<Product> {
Task<IPaginated<Product>> SearchAsync(ProductSearchDto search);
}
public class ProductRepository : ReadRepositoryAsync<Product>, IProductRepository {
public ProductRepository(DbContext context) : base(context) { }
public async Task<IPaginated<Product>> SearchAsync(ProductSearchDto search) {
var spec = SpecBuilder<Product>.Create()
.And(p => p.IsActive)
.AndIf(!string.IsNullOrEmpty(search.Category), p => p.Category == search.Category)
.AndIf(search.MinPrice.HasValue, p => p.Price >= search.MinPrice.Value)
.WithPagination(search.Pagination);
// Use built-in repository method with specification
return await GetPaginatedAsync(spec);
}
}
License
Licensed under the Apache License 2.0. See LICENSE file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Myth.Commons (>= 4.3.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Myth.Specification:
| Package | Downloads |
|---|---|
|
Myth.Repository
Generic repository pattern interfaces with async support, specification integration, and pagination. Provides read/write separation, CRUD operations, and extensible repository contracts for clean data access architecture. |
|
|
Myth.Repository.EntityFramework
Entity Framework Core implementations of repository pattern with Unit of Work, specification support, expression handling, and transaction management for robust data access with EF Core. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.3.0 | 0 | 2/1/2026 |
| 4.3.0-preview.3 | 32 | 2/1/2026 |
| 4.3.0-preview.2 | 137 | 12/22/2025 |
| 4.2.1-preview.1 | 620 | 12/2/2025 |
| 4.2.0 | 457 | 11/30/2025 |
| 4.2.0-preview.1 | 67 | 11/29/2025 |
| 4.1.0 | 368 | 11/27/2025 |
| 4.1.0-preview.3 | 138 | 11/27/2025 |
| 4.1.0-preview.2 | 135 | 11/27/2025 |
| 4.1.0-preview.1 | 141 | 11/26/2025 |
| 4.0.1 | 197 | 11/22/2025 |
| 4.0.1-preview.8 | 154 | 11/22/2025 |
| 4.0.1-preview.7 | 160 | 11/22/2025 |
| 4.0.1-preview.6 | 143 | 11/22/2025 |
| 4.0.1-preview.5 | 208 | 11/21/2025 |
| 4.0.1-preview.4 | 210 | 11/21/2025 |
| 4.0.1-preview.3 | 215 | 11/21/2025 |
| 4.0.1-preview.2 | 244 | 11/21/2025 |
| 4.0.1-preview.1 | 252 | 11/21/2025 |
| 4.0.0 | 428 | 11/20/2025 |
| 4.0.0-preview.3 | 351 | 11/19/2025 |
| 4.0.0-preview.2 | 93 | 11/15/2025 |
| 4.0.0-preview.1 | 120 | 11/15/2025 |
| 3.10.0 | 217 | 11/15/2025 |
| 3.0.5-preview.15 | 157 | 11/14/2025 |
| 3.0.5-preview.14 | 226 | 11/12/2025 |
| 3.0.5-preview.13 | 232 | 11/12/2025 |
| 3.0.5-preview.12 | 230 | 11/11/2025 |
| 3.0.5-preview.11 | 231 | 11/11/2025 |
| 3.0.5-preview.10 | 231 | 11/11/2025 |
| 3.0.5-preview.9 | 221 | 11/10/2025 |
| 3.0.5-preview.8 | 101 | 11/8/2025 |
| 3.0.5-preview.7 | 98 | 11/8/2025 |
| 3.0.5-preview.6 | 100 | 11/8/2025 |
| 3.0.5-preview.5 | 99 | 11/8/2025 |
| 3.0.5-preview.4 | 67 | 11/7/2025 |
| 3.0.5-preview.3 | 149 | 11/4/2025 |
| 3.0.5-preview.2 | 142 | 11/4/2025 |
| 3.0.5-preview.1 | 142 | 11/4/2025 |
| 3.0.4 | 267 | 11/3/2025 |
| 3.0.4-preview.19 | 84 | 11/2/2025 |
| 3.0.4-preview.17 | 81 | 11/1/2025 |
| 3.0.4-preview.16 | 82 | 11/1/2025 |
| 3.0.4-preview.15 | 77 | 10/31/2025 |
| 3.0.4-preview.14 | 141 | 10/31/2025 |
| 3.0.4-preview.13 | 141 | 10/30/2025 |
| 3.0.4-preview.12 | 132 | 10/23/2025 |
| 3.0.4-preview.11 | 134 | 10/23/2025 |
| 3.0.4-preview.10 | 128 | 10/23/2025 |
| 3.0.4-preview.9 | 128 | 10/23/2025 |
| 3.0.4-preview.8 | 132 | 10/22/2025 |
| 3.0.4-preview.6 | 128 | 10/21/2025 |
| 3.0.4-preview.5 | 127 | 10/21/2025 |
| 3.0.4-preview.4 | 131 | 10/20/2025 |
| 3.0.4-preview.3 | 129 | 10/20/2025 |
| 3.0.4-preview.2 | 48 | 10/18/2025 |
| 3.0.4-preview.1 | 131 | 10/7/2025 |
| 3.0.3 | 270 | 8/30/2025 |
| 3.0.2 | 178 | 8/23/2025 |
| 3.0.2-preview.4 | 134 | 8/21/2025 |
| 3.0.2-preview.3 | 108 | 8/16/2025 |
| 3.0.2-preview.1 | 84 | 5/23/2025 |
| 3.0.1 | 6,378 | 3/12/2025 |
| 3.0.1-preview.2 | 165 | 3/11/2025 |
| 3.0.1-preview.1 | 94 | 2/5/2025 |
| 3.0.0.2-preview | 151 | 12/10/2024 |
| 3.0.0.1-preview | 191 | 6/29/2024 |
| 3.0.0 | 4,644 | 12/10/2024 |
| 3.0.0-preview | 185 | 6/28/2024 |
| 2.0.0.17 | 2,363 | 12/15/2023 |
| 2.0.0.16 | 15,098 | 12/15/2023 |
| 2.0.0.15 | 332 | 12/15/2023 |
| 2.0.0.11 | 5,680 | 8/11/2022 |
| 2.0.0.10 | 2,999 | 7/20/2022 |
| 2.0.0.9 | 3,104 | 7/15/2022 |
| 2.0.0.8 | 3,118 | 7/12/2022 |
| 2.0.0.7 | 3,042 | 6/20/2022 |
| 2.0.0.6 | 3,158 | 5/23/2022 |
| 2.0.0.5 | 3,125 | 5/18/2022 |
| 2.0.0 | 3,212 | 2/17/2022 |