Myth.Specification
4.4.3
dotnet add package Myth.Specification --version 4.4.3
NuGet\Install-Package Myth.Specification -Version 4.4.3
paket add Myth.Specification --version 4.4.3
#r "nuget: Myth.Specification, 4.4.3"
#:package Myth.Specification@4.4.3
#addin nuget:?package=Myth.Specification&version=4.4.3
#tool nuget:?package=Myth.Specification&version=4.4.3
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
Expressionand 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.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 IsActive(this ISpec spec) {
return spec.And(p => p.IsActive);
}
public static ISpec InCategory(this ISpec spec, string category) {
return spec.And(p => p.Category == category);
}
public static ISpec PriceRange(this ISpec spec, decimal min, decimal max) {
return spec.And(p => p.Price >= min && p.Price <= max);
}
public static ISpec OrderByPrice(this ISpec spec, bool descending = false) {
return descending
? spec.OrderDescending(p => p.Price)
: spec.Order(p => p.Price);
}
}
// Usage
var spec = SpecBuilder.Create()
.IsActive()
.InCategory("Electronics")
.PriceRange(100, 1000)
.OrderByPrice(descending: true)
.Take(20);
var products = dbContext.Products.Specify(spec).ToList();
ISpec Interface
The core interface for all specifications.
Properties
Expression> Predicate { get; } // Filter expression
Func Query { get; } // Compiled predicate
Func, IOrderedQueryable> Sort { get; } // Ordering function
Func, IQueryable> 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 And(ISpec specification)
ISpec And(Expression> expression)
ISpec AndIf(bool condition, ISpec other)
ISpec AndIf(bool condition, Expression> other)
ISpec Or(ISpec specification)
ISpec Or(Expression> expression)
ISpec OrIf(bool condition, ISpec other)
ISpec OrIf(bool condition, Expression> other)
ISpec Not()
Ordering
ISpec Order(Expression> property)
ISpec OrderDescending(Expression> property)
Pagination
ISpec Skip(int amount)
ISpec Take(int amount)
ISpec WithPagination(Pagination pagination) // Uses Myth.Commons Pagination value object
Distinct
ISpec DistinctBy(Expression> property)
Query Execution
IQueryable Prepare(IQueryable query) // Apply filter + sort + post-process
IQueryable Filtered(IQueryable query) // Apply only filter
IQueryable Sorted(IQueryable query) // Apply only sorting
IQueryable Processed(IQueryable query) // Apply only post-processing
T? SatisfyingItemFrom(IQueryable query) // Get first matching item
IQueryable SatisfyingItemsFrom(IQueryable query) // Get all matching items
Validation
bool IsSatisfiedBy(T entity) // Check if entity satisfies specification (in-memory)
Initialization
ISpec InitEmpty() // Reset to empty specification
SpecBuilder
Abstract base class for creating specifications with a fluent API.
Creating Specifications
// Start with empty specification
var spec = SpecBuilder.Create();
// Chain operations
var spec = SpecBuilder.Create()
.And(p => p.IsActive)
.And(p => p.Price > 0)
.Order(p => p.Name)
.Take(10);
Implicit Conversion
SpecBuilder → Expression>
// Can be used directly where Expression is expected
Expression> expr = SpecBuilder.Create()
.And(p => p.IsActive);
Logical Operations
AND
var spec = SpecBuilder.Create()
.And(p => p.IsActive)
.And(p => p.Price > 100)
.And(p => p.Stock > 0);
// Conditional AND
var spec = SpecBuilder.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.Create()
.Or(p => p.Category == "Electronics")
.Or(p => p.Category == "Computers");
// Conditional OR
var spec = SpecBuilder.Create()
.And(p => p.IsActive)
.OrIf(includeDiscounted, p => p.IsDiscounted);
NOT
var activeSpec = SpecBuilder.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.Create()
.And(p => p.IsActive)
.And(p => p.Price > 100);
var spec2 = SpecBuilder.Create()
.And(p => p.IsDiscounted)
.And(p => p.Stock > 0);
var combinedSpec = spec1.Or(spec2);
Ordering
Single Level
// Ascending
var spec = SpecBuilder.Create()
.And(p => p.IsActive)
.Order(p => p.Name);
// Descending
var spec = SpecBuilder.Create()
.And(p => p.IsActive)
.OrderDescending(p => p.Price);
Multi-Level Ordering
The specification automatically uses ThenBy/ThenByDescending for subsequent ordering:
var spec = SpecBuilder.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.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.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.Create()
.WithPagination(Pagination.All);
// Default pagination (page 1, size 10)
var spec = SpecBuilder.Create()
.WithPagination(Pagination.Default);
Distinct Operations
// Get distinct products by brand
var spec = SpecBuilder.Create()
.And(p => p.IsActive)
.DistinctBy(p => p.Brand);
// Get distinct products by category and brand combination
var spec = SpecBuilder.Create()
.DistinctBy(p => new { p.Category, p.Brand });
Extension Methods
IQueryable Extensions
// Apply filter only
IQueryable Filter(this IQueryable values, ISpec spec)
// Apply sort only
IQueryable Sort(this IQueryable values, ISpec spec)
// Apply pagination only
IQueryable Paginate(this IQueryable values, ISpec spec)
IEnumerable Extensions
// Filter using compiled query
IEnumerable Where(this IEnumerable values, ISpec spec)
// Apply complete specification (convert to IQueryable first)
IQueryable Specify(this IEnumerable values, ISpec 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 { /* ... */ };
var filtered = memoryList.Where(spec); // Uses compiled predicate
Query Execution Methods
Prepare
Applies filter + sort + post-processing in sequence:
var spec = SpecBuilder.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.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 ForCustomer(this ISpec spec, Guid customerId) {
return spec.And(o => o.CustomerId == customerId);
}
public static ISpec WithStatus(this ISpec spec, params OrderStatus[] statuses) {
return spec.And(o => statuses.Contains(o.Status));
}
public static ISpec CreatedAfter(this ISpec spec, DateTime date) {
return spec.And(o => o.CreatedAt >= date);
}
public static ISpec MinimumAmount(this ISpec spec, decimal amount) {
return spec.And(o => o.TotalAmount >= amount);
}
public static ISpec RecentFirst(this ISpec spec) {
return spec.OrderDescending(o => o.CreatedAt);
}
public static ISpec SearchByNumber(this ISpec 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> SearchOrders(OrderSearchDto search) {
var spec = SpecBuilder.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(
pageNumber: search.Pagination.PageNumber,
pageSize: search.Pagination.PageSize,
totalItems: totalCount,
totalPages: (int)Math.Ceiling((double)totalCount / search.Pagination.PageSize),
items: orders);
}
public async Task HasPendingOrders(Guid customerId) {
var spec = SpecBuilder.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 Search([FromQuery] OrderSearchDto search) {
var result = await _repository.SearchOrders(search);
return Ok(result);
}
[HttpGet("validate/{id}")]
public async Task Validate(Guid id) {
var order = await _repository.GetByIdAsync(id);
if (order == null)
return NotFound();
var validSpec = SpecBuilder.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 - 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 {
Task> SearchAsync(ProductSearchDto search);
}
public class ProductRepository : ReadRepositoryAsync, IProductRepository {
public ProductRepository(DbContext context) : base(context) { }
public async Task> SearchAsync(ProductSearchDto search) {
var spec = SpecBuilder.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.4.3)
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.4.3 | 287 | 6/9/2026 |
| 4.4.2 | 157 | 6/7/2026 |
| 4.4.1 | 163 | 6/5/2026 |
| 4.4.0 | 158 | 6/5/2026 |
| 4.4.0-preview.12 | 77 | 6/4/2026 |
| 4.4.0-preview.11 | 88 | 6/4/2026 |
| 4.4.0-preview.10 | 94 | 6/2/2026 |
| 4.4.0-preview.9 | 80 | 6/2/2026 |
| 4.4.0-preview.8 | 111 | 3/12/2026 |
| 4.4.0-preview.7 | 87 | 3/12/2026 |
| 4.4.0-preview.6 | 97 | 2/20/2026 |
| 4.4.0-preview.5 | 94 | 2/19/2026 |
| 4.4.0-preview.4 | 91 | 2/18/2026 |
| 4.4.0-preview.3 | 101 | 2/18/2026 |
| 4.4.0-preview.2 | 96 | 2/17/2026 |
| 4.4.0-preview.1 | 101 | 2/14/2026 |
| 4.3.0 | 189 | 2/1/2026 |
| 4.3.0-preview.3 | 111 | 2/1/2026 |
| 4.3.0-preview.2 | 196 | 12/22/2025 |
| 4.2.1-preview.1 | 665 | 12/2/2025 |