DesignPatterns.Specification.Net
26.2.11.1
dotnet add package DesignPatterns.Specification.Net --version 26.2.11.1
NuGet\Install-Package DesignPatterns.Specification.Net -Version 26.2.11.1
<PackageReference Include="DesignPatterns.Specification.Net" Version="26.2.11.1" />
<PackageVersion Include="DesignPatterns.Specification.Net" Version="26.2.11.1" />
<PackageReference Include="DesignPatterns.Specification.Net" />
paket add DesignPatterns.Specification.Net --version 26.2.11.1
#r "nuget: DesignPatterns.Specification.Net, 26.2.11.1"
#:package DesignPatterns.Specification.Net@26.2.11.1
#addin nuget:?package=DesignPatterns.Specification.Net&version=26.2.11.1
#tool nuget:?package=DesignPatterns.Specification.Net&version=26.2.11.1
DesignPatterns.Specification.Net
A lightweight, fluent Specification Pattern library for .NET that helps you build reusable, composable, and testable query logic. Works seamlessly with Entity Framework Core and in-memory collections.
๐ฆ Installation
# Core library
dotnet add package DesignPatterns.Specification.Net
# Entity Framework Core integration
dotnet add package DesignPatterns.Specification.Net.EntityFrameworkCore
๐ฏ Why Use This Library?
The Specification pattern helps you:
- Encapsulate query logic in reusable, testable classes
- Eliminate duplicate queries across your application
- Compose complex queries using fluent, readable syntax
- Separate concerns by keeping query logic out of repositories and services
- Test easily with in-memory evaluation
- Apply consistently to both EF Core and in-memory collections
๐ Quick Start
1. Define a Specification
using DesignPatterns.Specification.Net;
public class ActiveCustomersSpec : Specification<Customer>
{
public ActiveCustomersSpec()
{
Query
.Where(c => c.IsActive)
.OrderBy(c => c.Name);
}
}
2. Use with Entity Framework Core
// Apply specification to DbSet
var activeCustomers = await dbContext.Customers
.WithSpecification(new ActiveCustomersSpec())
.ToListAsync(cancellationToken);
// Or use the direct extension
var activeCustomers = await dbContext.Customers
.ToListAsync(new ActiveCustomersSpec(), cancellationToken);
3. Use with In-Memory Collections
var customers = new List<Customer> { /* ... */ };
// Evaluate specification against in-memory data
var activeCustomers = new ActiveCustomersSpec().Evaluate(customers);
๐จ Features
โ Fluent Query Builder
Build complex queries with an intuitive fluent API:
public class CustomerSearchSpec : Specification<Customer>
{
public CustomerSearchSpec(string searchTerm, int page, int pageSize)
{
Query
.Where(c => c.IsActive)
.Search(c => c.Name, $"%{searchTerm}%")
.Search(c => c.Email, $"%{searchTerm}%")
.OrderBy(c => c.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Include(c => c.Orders)
.ThenInclude(o => o.OrderItems)
.AsNoTracking();
}
}
โ Projection Support
Transform query results with type-safe projections:
public class CustomerNamesSpec : Specification<Customer, string>
{
public CustomerNamesSpec()
{
Query
.Where(c => c.IsActive)
.Select(c => c.Name)
.OrderBy(name => name);
}
}
// Usage
var names = await dbContext.Customers
.ToListAsync(new CustomerNamesSpec(), cancellationToken);
โ Single Result Queries
Optimize queries that return a single result:
public class CustomerByIdSpec : SingleResultSpecification<Customer>
{
public CustomerByIdSpec(int id)
{
Query
.Where(c => c.Id == id)
.Include(c => c.Orders);
}
}
// Usage
var customer = await repository.FirstOrDefaultAsync(
new CustomerByIdSpec(customerId),
cancellationToken
);
โ Advanced EF Core Features
public class OptimizedOrdersSpec : Specification<Order>
{
public OptimizedOrdersSpec()
{
Query
.Where(o => o.Total > 1000)
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.AsNoTracking() // No change tracking
.AsSplitQuery() // Split into multiple SQL queries
.IgnoreQueryFilters(); // Ignore global query filters
}
}
โ Search with Pattern Matching
Supports SQL LIKE patterns (%, _, [A-Z], [^...]):
public class CustomerSearchSpec : Specification<Customer>
{
public CustomerSearchSpec(string pattern)
{
Query.Search(c => c.Name, pattern);
// Translates to EF.Functions.Like in EF Core
// Uses custom Like extension for in-memory collections
}
}
โ Post-Processing Actions
Execute logic after materialization:
public class CustomersWithComputedDataSpec : Specification<Customer>
{
public CustomersWithComputedDataSpec()
{
Query
.Where(c => c.IsActive)
.PostProcessingAction(customers =>
{
foreach (var customer in customers)
{
customer.FullName = $"{customer.FirstName} {customer.LastName}";
}
});
}
}
โ Caching Support
Enable caching for frequently used specifications:
public class CachedCustomersSpec : Specification<Customer>
{
public CachedCustomersSpec()
{
Query
.Where(c => c.IsActive)
.EnableCache("ActiveCustomers", nameof(Customer));
}
}
๐ Core Concepts
ISpecification<T>
The main interface that represents a specification for type T. Contains all query rules, options, and metadata.
Specification<T>
Base class for creating specifications. Use the Query property to build your specification using the fluent API.
SingleResultSpecification<T>
Specialized base class optimized for queries that return a single result (e.g., by ID or unique key).
Specification<T, TResult>
Base class for specifications with projections, transforming T to TResult.
SpecificationBuilder
Fluent API for composing query rules:
- Where: Filter criteria
- Include/ThenInclude: Eager loading
- OrderBy/ThenBy/OrderByDescending/ThenByDescending: Sorting
- Skip/Take: Pagination
- Search: Pattern-based search
- Select/SelectMany: Projection
- PostProcessingAction: Post-materialization logic
๐๏ธ Repository Integration
Using the Provided Repository Base Classes
// Entity Framework Core repository
public class CustomerRepository : RepositoryBase<Customer>
{
public CustomerRepository(AppDbContext context) : base(context)
{
}
}
// Usage
public class CustomerService
{
private readonly CustomerRepository _repository;
public CustomerService(CustomerRepository repository)
{
_repository = repository;
}
public async Task<List<Customer>> GetActiveCustomersAsync(CancellationToken ct)
{
return await _repository.ListAsync(new ActiveCustomersSpec(), ct);
}
public async Task<Customer?> GetCustomerByIdAsync(int id, CancellationToken ct)
{
return await _repository.FirstOrDefaultAsync(new CustomerByIdSpec(id), ct);
}
public async Task<int> CountActiveCustomersAsync(CancellationToken ct)
{
return await _repository.CountAsync(new ActiveCustomersSpec(), ct);
}
}
Using IDbContextFactory (for long-lived services)
public class CustomerRepository : ContextFactoryRepositoryBase<Customer, AppDbContext>
{
public CustomerRepository(IDbContextFactory<AppDbContext> factory)
: base(factory)
{
}
}
๐ง Advanced Scenarios
Combining Multiple Specifications
public class ActivePremiumCustomersSpec : Specification<Customer>
{
public ActivePremiumCustomersSpec()
{
Query
.Where(c => c.IsActive)
.Where(c => c.IsPremium)
.Where(c => c.TotalPurchases > 10000);
}
}
Dynamic Specifications
public class CustomerFilterSpec : Specification<Customer>
{
public CustomerFilterSpec(CustomerFilter filter)
{
if (filter.IsActive.HasValue)
Query.Where(c => c.IsActive == filter.IsActive.Value);
if (!string.IsNullOrEmpty(filter.SearchTerm))
Query.Search(c => c.Name, $"%{filter.SearchTerm}%");
if (filter.MinPurchases.HasValue)
Query.Where(c => c.TotalPurchases >= filter.MinPurchases.Value);
Query
.OrderBy(c => c.Name)
.Skip((filter.Page - 1) * filter.PageSize)
.Take(filter.PageSize);
}
}
Complex Includes
public class OrderWithDetailsSpec : Specification<Order>
{
public OrderWithDetailsSpec(int orderId)
{
Query
.Where(o => o.Id == orderId)
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.ThenInclude(p => p.Category)
.AsNoTracking();
}
}
Using Custom Evaluators
// Create a custom in-memory evaluator
var customEvaluator = new InMemorySpecificationEvaluator(
whereEvaluator: new WhereEvaluator(),
orderEvaluator: new OrderEvaluator(),
paginationEvaluator: new PaginationEvaluator()
);
// Use in specification
public class CustomSpec : Specification<Customer>
{
public CustomSpec() : base(customEvaluator)
{
Query.Where(c => c.IsActive);
}
}
โก Performance Tips
1. Use Cached Include Evaluator (EF Core)
For specifications with complex include chains:
// Configure in your DI container
services.AddSingleton<ISpecificationEvaluator>(
SpecificationEvaluator.Cached
);
2. Disable Tracking When Appropriate
Query.AsNoTracking(); // For read-only queries
3. Use Split Queries for Large Includes
Query
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.AsSplitQuery(); // Generates multiple SQL queries
4. Optimize Count/Any Operations
Repositories automatically optimize these operations by excluding includes and projections.
๐งช Testing
Unit Testing with In-Memory Evaluation
[Fact]
public void ActiveCustomersSpec_ShouldFilterActiveCustomers()
{
// Arrange
var customers = new List<Customer>
{
new() { Id = 1, Name = "Alice", IsActive = true },
new() { Id = 2, Name = "Bob", IsActive = false },
new() { Id = 3, Name = "Charlie", IsActive = true }
};
var spec = new ActiveCustomersSpec();
// Act
var result = spec.Evaluate(customers).ToList();
// Assert
Assert.Equal(2, result.Count);
Assert.All(result, c => Assert.True(c.IsActive));
}
Integration Testing with EF Core
[Fact]
public async Task CustomerRepository_WithActiveSpec_ReturnsActiveCustomers()
{
// Arrange
using var context = CreateInMemoryDbContext();
await SeedTestDataAsync(context);
var repository = new CustomerRepository(context);
// Act
var result = await repository.ListAsync(
new ActiveCustomersSpec(),
CancellationToken.None
);
// Assert
Assert.All(result, c => Assert.True(c.IsActive));
}
๐จ Common Pitfalls & Best Practices
โ Avoid: Multiple OrderBy Calls
// DON'T: This will throw DuplicateOrderChainException
Query
.OrderBy(c => c.Name)
.OrderBy(c => c.Email); // โ Wrong!
// DO: Use ThenBy for secondary ordering
Query
.OrderBy(c => c.Name)
.ThenBy(c => c.Email); // โ
Correct!
โ Avoid: Multiple Skip or Take Calls
// DON'T: This will throw exceptions
Query.Skip(10).Skip(20); // โ DuplicateSkipException
Query.Take(10).Take(20); // โ DuplicateTakeException
โ Avoid: Using Includes for Count/Any
// DON'T: Includes are ignored in count operations anyway
public class CustomerCountSpec : Specification<Customer>
{
public CustomerCountSpec()
{
Query
.Where(c => c.IsActive)
.Include(c => c.Orders); // โ Unnecessary for counting
}
}
// DO: Keep count specs simple
public class ActiveCustomerCountSpec : Specification<Customer>
{
public ActiveCustomerCountSpec()
{
Query.Where(c => c.IsActive); // โ
Clean and efficient
}
}
โ Do: Use AsNoTracking for Read-Only Queries
public class ReadOnlyCustomersSpec : Specification<Customer>
{
public ReadOnlyCustomersSpec()
{
Query
.Where(c => c.IsActive)
.AsNoTracking(); // โ
Better performance
}
}
โ Do: Keep Specifications Focused
// โ
Good: Single responsibility
public class ActiveCustomersSpec : Specification<Customer>
{
public ActiveCustomersSpec()
{
Query.Where(c => c.IsActive);
}
}
// โ
Good: Compose when needed
public class ActivePremiumCustomersSpec : Specification<Customer>
{
public ActivePremiumCustomersSpec()
{
Query
.Where(c => c.IsActive)
.Where(c => c.IsPremium);
}
}
๐ API Reference
Query Builder Methods
| Method | Description |
|---|---|
Where(predicate) |
Add filter criteria |
OrderBy(keySelector) |
Primary ascending sort |
OrderByDescending(keySelector) |
Primary descending sort |
ThenBy(keySelector) |
Secondary ascending sort |
ThenByDescending(keySelector) |
Secondary descending sort |
Include(navigationProperty) |
Eager load related entity |
ThenInclude(navigationProperty) |
Eager load nested related entity |
Skip(count) |
Skip specified number of records |
Take(count) |
Take specified number of records |
Search(selector, pattern) |
Pattern-based search (LIKE) |
Select(selector) |
Project to different type |
SelectMany(selector) |
Flatten and project |
PostProcessingAction(action) |
Execute after materialization |
AsTracking() |
Enable change tracking (EF Core) |
AsNoTracking() |
Disable change tracking (EF Core) |
AsNoTrackingWithIdentityResolution() |
No tracking with identity resolution |
AsSplitQuery() |
Use multiple SQL queries for includes |
IgnoreQueryFilters() |
Ignore global query filters |
EnableCache(key, args) |
Enable result caching |
Repository Methods
| Method | Description |
|---|---|
ListAsync(spec, ct) |
Get all matching entities |
FirstOrDefaultAsync(spec, ct) |
Get first match or null |
SingleOrDefaultAsync(spec, ct) |
Get single match or null |
CountAsync(spec, ct) |
Count matching entities |
AnyAsync(spec, ct) |
Check if any match exists |
AddAsync(entity, ct) |
Add new entity |
UpdateAsync(entity, ct) |
Update existing entity |
DeleteAsync(entity, ct) |
Delete entity |
SaveChangesAsync(ct) |
Persist changes |
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments
This library provides a pragmatic, production-ready implementation of the Specification pattern for .NET, emphasizing:
- Reusability: Define query logic once, use everywhere
- Testability: Easy to test with in-memory evaluation
- Maintainability: Clear separation of query concerns
- Performance: Optimized for EF Core with caching options
- Type Safety: Strongly-typed fluent API
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- No dependencies.
-
net6.0
- No dependencies.
-
net7.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on DesignPatterns.Specification.Net:
| Package | Downloads |
|---|---|
|
DesignPatterns.Specification.Net.EntityFrameworkCore
Entity Framework Core integration for DesignPatterns.Specification.Net. Provides EF Core-aware evaluators (Where, Include/ThenInclude with optional delegate caching, Order, Pagination), search via EF.Functions.Like, DbSet/IQueryable extensions (WithSpecification, ToListAsync/ToEnumerableAsync), and repository bases (RepositoryBase, ContextFactoryRepositoryBase, EFRepositoryFactory) with support for AsNoTracking/AsNoTrackingWithIdentityResolution, AsSplitQuery, and IgnoreQueryFilters. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 26.2.11.1 | 102 | 2/12/2026 |