| name | myth-specification |
| description | Use when you need to build reusable, composable database queries. SpecBuilder<T>.Create() chains .And()/.Or()/.Not()/.AndIf()/.OrIf() with Expression<Func<T,bool>>, plus .Order()/.OrderDescending(), .Skip()/.Take(), and .DistinctBy(). Translates to SQL via EF Core and also supports in-memory validation. |
SKILL.md - Myth.Specification
Version: 1.0
Target Framework: .NET 8.0
License: Apache 2.0
Table of Contents
Overview
Myth.Specification implements the Specification Pattern (from Domain-Driven Design) to encapsulate business rules and query logic in reusable, composable objects. It provides a fluent API for building complex queries with filtering, sorting, and pagination.
Key Features
- Fluent API: Chainable methods for building complex specifications
- Composable: Combine specifications using AND, OR, NOT logic
- Expression Trees: Compatible with Entity Framework Core (translates to SQL)
- Sorting: Multi-level ordering with OrderBy/ThenBy support
- Pagination: Built-in support for Skip/Take and Pagination value objects
- Distinct: Remove duplicates by property
- In-Memory Validation: Validate entities against specifications
- Separation of Concerns: Keep business rules separate from repositories
Installation
dotnet add package Myth.Specification
Dependencies
- .NET 8.0 or higher
- Myth.Commons (for Pagination value object)
Core Concepts
1. Specification Pattern
Specifications encapsulate query logic as objects that can be:
- Composed: Combined using logical operators (AND, OR, NOT)
- Reused: Shared across multiple queries
- Tested: Unit tested in isolation
- Named: Give business meaning to complex queries
2. Expression Trees
Specifications use Expression<Func<T, bool>> which:
- Can be translated to SQL by Entity Framework
- Can be compiled for in-memory evaluation
- Maintain full type safety
3. Fluent Interface
All methods return ISpec<T> for method chaining:
var spec = SpecBuilder<User>.Create()
.And(u => u.IsActive)
.And(u => u.Role == "Admin")
.Order(u => u.Name)
.Skip(10)
.Take(20);
API Reference
ISpec Interface
Namespace: Myth.Interfaces
Core interface for all specifications.
Properties
public interface ISpec<T> {
Expression<Func<T, bool>> Predicate { get; }
Func<T, bool> Query { get; }
int ItemsSkiped { get; }
int ItemsTaked { get; }
Func<IQueryable<T>, IOrderedQueryable<T>> Sort { get; }
Func<IQueryable<T>, IQueryable<T>> PostProcess { get; }
Func<IQueryable<T>, IQueryable<T>>? Includes { get; }
}
Logical Methods
ISpec<T> And(ISpec<T> specification);
ISpec<T> And(Expression<Func<T, bool>> expression);
ISpec<T> AndIf(bool condition, ISpec<T> specification);
ISpec<T> AndIf(bool condition, Expression<Func<T, bool>> expression);
ISpec<T> Or(ISpec<T> specification);
ISpec<T> Or(Expression<Func<T, bool>> expression);
ISpec<T> OrIf(bool condition, ISpec<T> specification);
ISpec<T> OrIf(bool condition, Expression<Func<T, bool>> expression);
ISpec<T> Not();
Examples:
var spec = SpecBuilder<User>.Create()
.And(u => u.IsActive)
.And(u => u.Age >= 18);
var includeDeleted = false;
var spec = SpecBuilder<User>.Create()
.And(u => u.IsActive)
.AndIf(!includeDeleted, u => !u.IsDeleted);
var spec = SpecBuilder<User>.Create()
.And(u => u.IsActive)
.Or(u => u.Role == "Admin");
var spec = SpecBuilder<User>.Create()
.And(u => u.IsDeleted)
.Not();
Sorting Methods
ISpec<T> Order<TProperty>(Expression<Func<T, TProperty>> property);
ISpec<T> OrderDescending<TProperty>(Expression<Func<T, TProperty>> property);
Examples:
var spec = SpecBuilder<User>.Create()
.Order(u => u.Name);
var spec = SpecBuilder<User>.Create()
.Order(u => u.Department)
.Order(u => u.Name)
.OrderDescending(u => u.CreatedAt);
Pagination Methods
ISpec<T> Skip(int amount);
ISpec<T> Take(int amount);
ISpec<T> WithPagination(Pagination pagination);
Examples:
var spec = SpecBuilder<User>.Create()
.Skip(20)
.Take(10);
var pagination = new Pagination(pageNumber: 2, pageSize: 25);
var spec = SpecBuilder<User>.Create()
.WithPagination(pagination);
var spec = SpecBuilder<User>.Create()
.WithPagination(Pagination.Default);
var spec = SpecBuilder<User>.Create()
.WithPagination(Pagination.All);
Eager Loading Methods
ISpec<T> Include(Func<IQueryable<T>, IQueryable<T>> includeQuery);
Examples:
var spec = SpecBuilder<Project>
.Create()
.HasId(projectId)
.Include(q => q.Include(p => p.Members));
var spec = SpecBuilder<Project>
.Create()
.Include(q => q.Include(p => p.Members)
.Include(p => p.Tasks));
var spec = SpecBuilder<Project>
.Create()
.Include(q => q.Include(p => p.Members)
.ThenInclude(m => m.User)
.ThenInclude(u => u.Profile));
var spec = SpecBuilder<Order>
.Create()
.IsActive()
.Include(q => q.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Include(o => o.ShippingAddress));
Note: The Include() method uses EF Core's Include and ThenInclude extension methods. The includes are applied before filters in the query pipeline. This ensures that navigation properties are available for filtering and are loaded efficiently with the main query.
Post-Processing Methods
ISpec<T> DistinctBy<TProperty>(Expression<Func<T, TProperty>> property);
Example:
var spec = SpecBuilder<User>.Create()
.DistinctBy(u => u.Email);
Execution Methods
IQueryable<T> Prepare(IQueryable<T> query);
IQueryable<T> Included(IQueryable<T> query);
IQueryable<T> Filtered(IQueryable<T> query);
IQueryable<T> Sorted(IQueryable<T> query);
IQueryable<T> Processed(IQueryable<T> query);
T? SatisfyingItemFrom(IQueryable<T> query);
IQueryable<T> SatisfyingItemsFrom(IQueryable<T> query);
Examples:
var spec = SpecBuilder<Project>
.Create()
.And(p => p.IsActive)
.Include(q => q.Include(p => p.Members))
.Order(p => p.Name)
.Skip(10)
.Take(10);
var projects = spec.Prepare(_context.Projects).ToList();
var included = spec.Included(_context.Projects);
var filtered = spec.Filtered(_context.Projects);
var sorted = spec.Sorted(_context.Projects);
var paginated = spec.Processed(_context.Projects);
var project = spec.SatisfyingItemFrom(_context.Projects);
var query = spec.SatisfyingItemsFrom(_context.Projects);
var result = await query.ToListAsync();
Validation Methods
bool IsSatisfiedBy(T entity);
Example:
var spec = SpecBuilder<User>.Create()
.And(u => u.IsActive)
.And(u => !string.IsNullOrEmpty(u.Email))
.And(u => u.Age >= 18);
var user = new User { IsActive = true, Email = "test@example.com", Age = 25 };
var isValid = spec.IsSatisfiedBy(user);
Initialization Methods
ISpec<T> InitEmpty();
SpecBuilder
Namespace: Myth.Specifications
Factory class for creating specifications.
public abstract class SpecBuilder<T> : ISpec<T> {
public static ISpec<T> Create();
public static implicit operator Expression<Func<T, bool>>(SpecBuilder<T> spec);
}
Usage:
var spec = SpecBuilder<User>.Create();
Expression<Func<User, bool>> expression = spec;
Extension Methods
Namespace: Myth.Extensions
Extension methods for IEnumerable and IQueryable.
For IEnumerable
IEnumerable<T> Where<T>(this IEnumerable<T> values, ISpec<T> spec);
IQueryable<T> Specify<T>(this IEnumerable<T> values, ISpec<T> spec);
Examples:
var users = GetUsersFromMemory();
var active = users.Where(spec);
var result = users.Specify(spec).ToList();
For IQueryable
IQueryable<T> Filter<T>(this IQueryable<T> values, ISpec<T> spec);
IQueryable<T> Sort<T>(this IQueryable<T> values, ISpec<T> spec);
IQueryable<T> Paginate<T>(this IQueryable<T> values, ISpec<T> spec);
IQueryable<T> Specify<T>(this IQueryable<T> values, ISpec<T> spec);
Examples:
var query = _context.Users.AsQueryable();
var filtered = query.Filter(spec);
var sorted = query.Sort(spec);
var paginated = query.Paginate(spec);
var result = query.Specify(spec).ToList();
Exceptions
InvalidSpecificationException
Namespace: Myth.Exceptions
public sealed class InvalidSpecificationException : Exception {
public InvalidSpecificationException(string message)
}
When Thrown:
- When
IsSatisfiedBy() is called on specification with null Predicate
Example:
try {
var isValid = spec.IsSatisfiedBy(entity);
} catch (InvalidSpecificationException ex) {
_logger.LogError(ex, "Invalid specification");
}
SpecificationException
Namespace: Myth.Exceptions
public class SpecificationException : Exception {
public SpecificationException(string message, Exception? exception)
}
When Thrown:
- Error applying filter: "Error on apply filter specification!"
- Error applying sort: "Error on apply sort specification!"
- Error applying post-process: "Error on apply post process specification!"
Example:
try {
var result = spec.Prepare(query).ToList();
} catch (SpecificationException ex) {
_logger.LogError(ex, "Specification error: {Message}", ex.Message);
}
Usage Examples
Example 1: Basic User Query with Filters
public class UserRepository {
private readonly ApplicationDbContext _context;
public async Task<List<User>> GetActiveAdultsAsync() {
var spec = SpecBuilder<User>.Create()
.And(u => u.IsActive)
.And(u => u.Age >= 18)
.Order(u => u.Name);
return await _context.Users
.Specify(spec)
.ToListAsync();
}
}
Example 2: Reusable Specifications as Extension Methods
public static class UserSpecifications {
public static ISpec<User> IsActive(this ISpec<User> spec) {
return spec.And(u => u.IsActive && !u.IsDeleted);
}
public static ISpec<User> HasRole(this ISpec<User> spec, string role) {
return spec.And(u => u.Role == role);
}
public static ISpec<User> EmailContains(this ISpec<User> spec, string searchTerm) {
return spec.And(u => u.Email.Contains(searchTerm));
}
public static ISpec<User> CreatedAfter(this ISpec<User> spec, DateTime date) {
return spec.And(u => u.CreatedAt >= date);
}
public static ISpec<User> OrderByName(this ISpec<User> spec) {
return spec.Order(u => u.Name);
}
}
public async Task<List<User>> SearchUsersAsync(string searchTerm, string role) {
var spec = SpecBuilder<User>.Create()
.IsActive()
.HasRole(role)
.EmailContains(searchTerm)
.OrderByName()
.WithPagination(new Pagination(1, 20));
return await _context.Users.Specify(spec).ToListAsync();
}
Example 3: Conditional Filtering
public async Task<List<Product>> SearchProductsAsync(ProductSearchRequest request) {
var spec = SpecBuilder<Product>.Create();
spec = spec.And(p => p.IsActive);
spec = spec.AndIf(
!string.IsNullOrEmpty(request.Name),
p => p.Name.Contains(request.Name));
spec = spec.AndIf(
request.MinPrice.HasValue,
p => p.Price >= request.MinPrice!.Value);
spec = spec.AndIf(
request.MaxPrice.HasValue,
p => p.Price <= request.MaxPrice!.Value);
spec = spec.AndIf(
!string.IsNullOrEmpty(request.Category),
p => p.Category == request.Category);
if (request.SortBy == "price") {
spec = request.SortDescending
? spec.OrderDescending(p => p.Price)
: spec.Order(p => p.Price);
} else {
spec = spec.Order(p => p.Name);
}
spec = spec.WithPagination(new Pagination(
request.PageNumber,
request.PageSize));
return await _context.Products.Specify(spec).ToListAsync();
}
Example 4: Complex Business Rules
public static class OrderSpecifications {
public static ISpec<Order> CanBeShipped(this ISpec<Order> spec) {
return spec.And(o =>
o.Status == OrderStatus.Confirmed &&
o.PaymentStatus == PaymentStatus.Paid &&
!o.IsDeleted &&
o.Items.Any());
}
public static ISpec<Order> RequiresAttention(this ISpec<Order> spec) {
return spec.Or(o =>
(o.Status == OrderStatus.Pending && o.CreatedAt < DateTime.UtcNow.AddDays(-2)) ||
(o.Status == OrderStatus.Confirmed && o.PaymentStatus == PaymentStatus.Pending) ||
(o.Status == OrderStatus.Shipped && o.ShippedAt < DateTime.UtcNow.AddDays(-7)));
}
public static ISpec<Order> ForCustomer(this ISpec<Order> spec, Guid customerId) {
return spec.And(o => o.CustomerId == customerId);
}
public static ISpec<Order> InDateRange(
this ISpec<Order> spec,
DateTime startDate,
DateTime endDate) {
return spec.And(o => o.CreatedAt >= startDate && o.CreatedAt <= endDate);
}
}
public async Task<List<Order>> GetShippableOrdersAsync() {
var spec = SpecBuilder<Order>.Create()
.CanBeShipped()
.Order(o => o.Priority)
.OrderDescending(o => o.CreatedAt);
return await _context.Orders.Specify(spec).ToListAsync();
}
public async Task<List<Order>> GetOrdersRequiringAttentionAsync() {
var spec = SpecBuilder<Order>.Create()
.RequiresAttention()
.OrderDescending(o => o.CreatedAt);
return await _context.Orders.Specify(spec).ToListAsync();
}
Example 5: Paginated API Endpoint
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase {
private readonly ApplicationDbContext _context;
[HttpGet]
public async Task<ActionResult<PaginatedResponse<UserDto>>> GetUsers(
[FromQuery] Pagination pagination,
[FromQuery] string? search = null,
[FromQuery] string? role = null,
[FromQuery] bool? isActive = null) {
var spec = SpecBuilder<User>.Create();
spec = spec.AndIf(isActive.HasValue, u => u.IsActive == isActive!.Value);
spec = spec.AndIf(!string.IsNullOrEmpty(role), u => u.Role == role);
spec = spec.AndIf(!string.IsNullOrEmpty(search), u =>
u.Name.Contains(search!) ||
u.Email.Contains(search!));
var totalItems = await _context.Users.Filter(spec).CountAsync();
spec = spec.Order(u => u.Name).WithPagination(pagination);
var users = await _context.Users
.Specify(spec)
.Select(u => new UserDto {
Id = u.Id,
Name = u.Name,
Email = u.Email,
Role = u.Role
})
.ToListAsync();
return new PaginatedResponse<UserDto> {
Items = users,
PageNumber = pagination.PageNumber,
PageSize = pagination.PageSize,
TotalItems = totalItems,
TotalPages = (int)Math.Ceiling(totalItems / (double)pagination.PageSize)
};
}
}
Example 6: In-Memory Validation
public class UserValidator {
private readonly ISpec<User> _validUserSpec;
public UserValidator() {
_validUserSpec = SpecBuilder<User>.Create()
.And(u => !string.IsNullOrEmpty(u.Name))
.And(u => !string.IsNullOrEmpty(u.Email))
.And(u => u.Email.Contains("@"))
.And(u => u.Age >= 18)
.And(u => u.Age <= 120);
}
public bool IsValid(User user) {
return _validUserSpec.IsSatisfiedBy(user);
}
public List<string> Validate(User user) {
var errors = new List<string>();
if (string.IsNullOrEmpty(user.Name))
errors.Add("Name is required");
if (string.IsNullOrEmpty(user.Email))
errors.Add("Email is required");
else if (!user.Email.Contains("@"))
errors.Add("Email must be valid");
if (user.Age < 18)
errors.Add("User must be at least 18 years old");
if (user.Age > 120)
errors.Add("Invalid age");
return errors;
}
}
var user = new User { Name = "John", Email = "john@example.com", Age = 25 };
var validator = new UserValidator();
if (validator.IsValid(user)) {
await _repository.AddAsync(user);
} else {
var errors = validator.Validate(user);
return BadRequest(errors);
}
Example 7: Combining Specifications
public class ReportService {
public async Task<OrderReport> GenerateMonthlyReportAsync(
int year,
int month,
Guid? customerId = null) {
var startDate = new DateTime(year, month, 1);
var endDate = startDate.AddMonths(1);
var baseSpec = SpecBuilder<Order>.Create()
.And(o => o.CreatedAt >= startDate)
.And(o => o.CreatedAt < endDate);
var spec = baseSpec;
if (customerId.HasValue) {
spec = spec.And(o => o.CustomerId == customerId.Value);
}
var allOrders = await _context.Orders.Filter(spec).ToListAsync();
var completedSpec = spec.And(o => o.Status == OrderStatus.Completed);
var completedOrders = await _context.Orders.Filter(completedSpec).ToListAsync();
var cancelledSpec = spec.And(o => o.Status == OrderStatus.Cancelled);
var cancelledOrders = await _context.Orders.Filter(cancelledSpec).ToListAsync();
return new OrderReport {
Period = $"{year}-{month:D2}",
TotalOrders = allOrders.Count,
CompletedOrders = completedOrders.Count,
CancelledOrders = cancelledOrders.Count,
TotalRevenue = completedOrders.Sum(o => o.Total),
AverageOrderValue = completedOrders.Any()
? completedOrders.Average(o => o.Total)
: 0
};
}
}
Example 8: Eager Loading with Include
public static class ProjectSpecifications {
public static ISpec<Project> HasId(this ISpec<Project> spec, Guid id) =>
spec.And(p => p.Id == id);
public static ISpec<Project> IsActive(this ISpec<Project> spec) =>
spec.And(p => !p.IsDeleted && p.Status == ProjectStatus.Active);
public static ISpec<Project> WithMembers(this ISpec<Project> spec) =>
spec.Include(q => q.Include(p => p.Members));
public static ISpec<Project> WithMembersAndRoles(this ISpec<Project> spec) =>
spec.Include(q => q.Include(p => p.Members)
.ThenInclude(m => m.Role));
public static ISpec<Project> WithFullDetails(this ISpec<Project> spec) =>
spec.Include(q => q.Include(p => p.Members)
.ThenInclude(m => m.User)
.Include(p => p.Tasks)
.ThenInclude(t => t.Assignee)
.Include(p => p.Owner));
}
public class ProjectRepository {
private readonly ApplicationDbContext _context;
public async Task<Project?> GetByIdWithMembersAsync(Guid id, CancellationToken ct) {
var spec = SpecBuilder<Project>
.Create()
.HasId(id)
.IsActive()
.WithMembers();
return await _context.Projects
.Specify(spec)
.FirstOrDefaultAsync(ct);
}
public async Task<List<Project>> GetActiveProjectsWithDetailsAsync(
string? searchTerm,
Pagination pagination,
CancellationToken ct) {
var spec = SpecBuilder<Project>
.Create()
.IsActive()
.AndIf(!string.IsNullOrEmpty(searchTerm), p => p.Name.Contains(searchTerm!))
.WithFullDetails()
.Order(p => p.Name)
.WithPagination(pagination);
return await _context.Projects
.Specify(spec)
.ToListAsync(ct);
}
public async Task<Project?> GetProjectDetailsAsync(Guid projectId, CancellationToken ct) {
var spec = SpecBuilder<Project>
.Create()
.HasId(projectId)
.Include(q => q.Include(p => p.Members)
.ThenInclude(m => m.User)
.ThenInclude(u => u.Profile)
.Include(p => p.Tasks)
.Include(p => p.Documents));
return await _repository.FirstOrDefaultAsync(spec, ct);
}
}
Key Points:
.Include() is applied before filters in the query pipeline, ensuring navigation properties are eagerly loaded
- Multiple includes can be chained together
ThenInclude() loads nested navigation properties
- Includes work seamlessly with repository methods like
FirstOrDefaultAsync, SearchAsync, etc.
- Spec extensions can encapsulate common include patterns (e.g.,
WithFullDetails())
Advanced Patterns
Pattern 1: Specification Factory
public class UserSpecificationFactory {
public ISpec<User> CreateForRole(string role) {
return SpecBuilder<User>.Create()
.And(u => u.IsActive)
.And(u => u.Role == role)
.Order(u => u.Name);
}
public ISpec<User> CreateForSearch(string searchTerm) {
return SpecBuilder<User>.Create()
.And(u => u.IsActive)
.And(u =>
u.Name.Contains(searchTerm) ||
u.Email.Contains(searchTerm))
.Order(u => u.Name);
}
public ISpec<User> CreateForDepartment(string department) {
return SpecBuilder<User>.Create()
.And(u => u.IsActive)
.And(u => u.Department == department)
.Order(u => u.Department)
.Order(u => u.Name);
}
}
Pattern 2: Specification Composition
public class OrderQueryService {
private ISpec<Order> BaseSpec => SpecBuilder<Order>.Create()
.And(o => !o.IsDeleted);
private ISpec<Order> ActiveSpec => BaseSpec
.And(o => o.Status != OrderStatus.Cancelled);
private ISpec<Order> PendingSpec => ActiveSpec
.And(o => o.Status == OrderStatus.Pending);
private ISpec<Order> ShippableSpec => ActiveSpec
.And(o => o.Status == OrderStatus.Confirmed)
.And(o => o.PaymentStatus == PaymentStatus.Paid);
public async Task<List<Order>> GetPendingOrdersAsync() {
return await _context.Orders
.Specify(PendingSpec.Order(o => o.CreatedAt))
.ToListAsync();
}
public async Task<List<Order>> GetShippableOrdersAsync() {
return await _context.Orders
.Specify(ShippableSpec.OrderDescending(o => o.Priority))
.ToListAsync();
}
}
Pattern 3: Repository with Specifications
public interface IRepository<T> where T : class {
Task<List<T>> FindAsync(ISpec<T> specification);
Task<T?> FindOneAsync(ISpec<T> specification);
Task<int> CountAsync(ISpec<T> specification);
Task<bool> AnyAsync(ISpec<T> specification);
}
public class Repository<T> : IRepository<T> where T : class {
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(DbContext context) {
_context = context;
_dbSet = context.Set<T>();
}
public async Task<List<T>> FindAsync(ISpec<T> specification) {
return await _dbSet.Specify(specification).ToListAsync();
}
public async Task<T?> FindOneAsync(ISpec<T> specification) {
return await _dbSet.Filter(specification).FirstOrDefaultAsync();
}
public async Task<int> CountAsync(ISpec<T> specification) {
return await _dbSet.Filter(specification).CountAsync();
}
public async Task<bool> AnyAsync(ISpec<T> specification) {
return await _dbSet.Filter(specification).AnyAsync();
}
}
var spec = SpecBuilder<User>.Create()
.And(u => u.IsActive)
.Order(u => u.Name)
.Skip(20)
.Take(10);
var users = await _userRepository.FindAsync(spec);
var count = await _userRepository.CountAsync(spec);
Best Practices
1. Create Reusable Specifications in Static Classes
IMPORTANT: Always create specification extension methods in static classes named after your entity/model. This provides clear business meaning and improves code organization.
✅ DO:
public static class UserSpecifications {
public static ISpec<User> IsActive(this ISpec<User> spec) =>
spec.And(u => u.IsActive && !u.IsDeleted);
public static ISpec<User> HasRole(this ISpec<User> spec, string role) =>
spec.And(u => u.Role == role);
public static ISpec<User> CreatedAfter(this ISpec<User> spec, DateTime date) =>
spec.And(u => u.CreatedAt >= date);
}
var activeAdmins = SpecBuilder<User>.Create()
.IsActive()
.HasRole("Admin")
.CreatedAfter(DateTime.UtcNow.AddDays(-30));
❌ DON'T:
var spec1 = SpecBuilder<User>.Create().And(u => u.IsActive && !u.IsDeleted);
var spec2 = SpecBuilder<User>.Create().And(u => u.IsActive && !u.IsDeleted);
public class UserRepository {
public ISpec<User> IsActive() => ...
}
Benefits of Static Classes:
- Business Clarity:
UserSpecifications.IsActive() clearly expresses business intent
- Discoverability: IntelliSense shows all available specifications for the entity
- Organization: All business rules for an entity are in one place
- Reusability: Can be used across repositories, services, and controllers
- Testability: Easy to unit test specifications in isolation
2. Use Meaningful Names
✅ DO:
public static ISpec<Order> CanBeShipped(this ISpec<Order> spec) { }
public static ISpec<Order> RequiresAttention(this ISpec<Order> spec) { }
public static ISpec<Order> IsOverdue(this ISpec<Order> spec) { }
❌ DON'T:
public static ISpec<Order> Spec1(this ISpec<Order> spec) { }
public static ISpec<Order> Check(this ISpec<Order> spec) { }
3. Keep Specifications Focused
✅ DO:
public static ISpec<User> IsActive(this ISpec<User> spec) =>
spec.And(u => u.IsActive);
public static ISpec<User> IsAdult(this ISpec<User> spec) =>
spec.And(u => u.Age >= 18);
var spec = SpecBuilder<User>.Create()
.IsActive()
.IsAdult();
❌ DON'T:
public static ISpec<User> ComplexSpec(this ISpec<User> spec) =>
spec.And(u => u.IsActive && u.Age >= 18 && u.HasRole("Admin") && ...);
4. Separate Filter Count from Paginated Query
✅ DO:
var totalCount = await _context.Users.Filter(spec).CountAsync();
spec = spec.WithPagination(pagination);
var users = await _context.Users.Specify(spec).ToListAsync();
❌ DON'T:
spec = spec.WithPagination(pagination);
var count = await _context.Users.Specify(spec).CountAsync();
5. Use AndIf for Optional Filters
✅ DO:
var spec = SpecBuilder<Product>.Create()
.AndIf(!string.IsNullOrEmpty(name), p => p.Name.Contains(name))
.AndIf(minPrice.HasValue, p => p.Price >= minPrice!.Value)
.AndIf(maxPrice.HasValue, p => p.Price <= maxPrice!.Value);
❌ DON'T:
var spec = SpecBuilder<Product>.Create();
if (!string.IsNullOrEmpty(name))
spec = spec.And(p => p.Name.Contains(name));
if (minPrice.HasValue)
spec = spec.And(p => p.Price >= minPrice.Value);
6. Format Specification Chains on Multiple Lines
Break the SpecBuilder chain across multiple lines to improve readability. Each method call should be on its own line with consistent indentation.
✅ DO:
var spec = SpecBuilder<Idea>
.Create()
.HasId(command.IdeaId)
.IsActive()
.Order(i => i.CreatedAt);
❌ DON'T:
var spec = SpecBuilder<Idea>.Create().HasId(command.IdeaId).IsActive().Order(i => i.CreatedAt);
Benefits:
- Readability: Each filter is visible at a glance
- Diffs: Git diffs are cleaner when adding/removing individual steps
- Debugging: Easier to comment out a single step during debugging
7. Use Repository Methods Directly with Specifications
When the repository exposes methods that accept ISpec<T> directly (e.g., FirstOrDefaultAsync, ToListAsync, CountAsync), prefer those over manually chaining .Where(spec) on a queryable, as mixing EF Core query chains with specifications breaks the abstraction and makes the intent less clear.
✅ DO:
var spec = SpecBuilder<Project>
.Create()
.HasId(command.ProjectId)
.IsActive();
var project = await projectRepository.FirstOrDefaultAsync(spec, cancellationToken);
❌ DON'T:
var spec = SpecBuilder<Project>
.Create()
.HasId(command.ProjectId)
.IsActive();
var project = await projectRepository
.Where(spec)
.Include(p => p.Members)
.FirstOrDefaultAsync(cancellationToken);
Note: If you need to apply .Include() or other EF Core-specific operations, keep those inside the repository implementation, not in the caller. The caller should only provide the specification.
Troubleshooting
Issue 1: Expression Cannot Be Translated to SQL
Problem:
var spec = SpecBuilder<User>.Create()
.And(u => SomeLocalMethod(u.Name));
Cause: EF Core cannot translate custom methods to SQL.
Solution:
var spec = SpecBuilder<User>.Create()
.And(u => u.Name.Contains("search"));
var users = await _context.Users.ToListAsync();
var filtered = users.Where(spec);
Issue 2: InvalidSpecificationException
Problem: IsSatisfiedBy throws exception.
Cause: Predicate is null (using NullSpec or empty spec).
Solution:
var spec = SpecBuilder<User>.Create()
.And(u => u.IsActive);
var isValid = spec.IsSatisfiedBy(user);
Issue 3: Ordering Not Applied
Problem: Results not sorted as expected.
Cause: Forgetting to call Sort() or Specify().
Solution:
var users = await _context.Users.Filter(spec).ToListAsync();
var users = await _context.Users.Specify(spec).ToListAsync();
var users = await _context.Users.Filter(spec).Sort(spec).ToListAsync();
Issue 4: Pagination Not Working with Pagination.All
Problem: All items returned instead of paginated.
Cause: Pagination.All explicitly disables pagination.
Solution:
var pagination = request.GetAll
? Pagination.All
: new Pagination(request.PageNumber, request.PageSize);
Performance Considerations
- Filter Before Sort: EF Core can optimize filtered queries better
- Count Before Pagination: Get total count before applying Skip/Take
- Projection: Use Select() after specifications to reduce data transfer
- AsNoTracking: Use for read-only queries
- Compiled Queries: Consider for frequently used specifications
var totalCount = await _context.Users.Filter(spec).CountAsync();
var users = await _context.Users
.AsNoTracking()
.Specify(spec)
.Select(u => new UserDto { Id = u.Id, Name = u.Name })
.ToListAsync();
Integration with Myth Ecosystem
using Myth.Extensions;
using Myth.Interfaces;
using Myth.Specifications;
using Myth.ValueObjects;
public class UserRepository : IUserRepository {
public async Task<IPaginated<User>> GetPaginatedAsync(
ISpec<User> specification,
Pagination pagination) {
var spec = specification.WithPagination(pagination);
var users = await _context.Users.Specify(spec).ToListAsync();
return new Paginated<User>(
pagination.PageNumber,
pagination.PageSize,
await _context.Users.Filter(specification).CountAsync(),
,
users);
}
}
Summary
Myth.Specification provides:
- ✅ Reusable Business Rules: Encapsulate query logic
- ✅ Composable: Combine with AND, OR, NOT
- ✅ Type Safe: Full IntelliSense and compile-time checking
- ✅ EF Core Compatible: Translates to SQL
- ✅ Testable: Unit test specifications in isolation
- ✅ Maintainable: Change business rules in one place
Additional Resources
This documentation is maintained for AI agents and developers. For questions or contributions, please refer to the repository.