Myth.Specification 4.3.0

dotnet add package Myth.Specification --version 4.3.0
                    
NuGet\Install-Package Myth.Specification -Version 4.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Myth.Specification" Version="4.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Myth.Specification" Version="4.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Myth.Specification" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Myth.Specification --version 4.3.0
                    
#r "nuget: Myth.Specification, 4.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Myth.Specification@4.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Myth.Specification&version=4.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Myth.Specification&version=4.3.0
                    
Install as a Cake Tool

Myth.Specification

NuGet Version NuGet Version

License

pt-br en

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 filter
  • Sorted() - Error applying sort
  • Processed() - Error applying post-processing

Best Practices

  1. Create Extension Methods - Define reusable specifications as extension methods on ISpec<T>
  2. Keep Specifications Simple - Each specification should represent a single business rule
  3. Use Conditional Composition - Use AndIf/OrIf for optional filters
  4. Separate Concerns - Keep query logic separate from repository implementation
  5. Test Specifications - Use IsSatisfiedBy() for unit testing specifications
  6. Use Pagination Value Objects - Leverage Myth.Commons.Pagination for consistent paging
  7. Name Meaningfully - Use descriptive names like IsActive(), RecentFirst()
  8. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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