DesignPatterns.Specification.Net.EntityFrameworkCore
26.2.11.1
dotnet add package DesignPatterns.Specification.Net.EntityFrameworkCore --version 26.2.11.1
NuGet\Install-Package DesignPatterns.Specification.Net.EntityFrameworkCore -Version 26.2.11.1
<PackageReference Include="DesignPatterns.Specification.Net.EntityFrameworkCore" Version="26.2.11.1" />
<PackageVersion Include="DesignPatterns.Specification.Net.EntityFrameworkCore" Version="26.2.11.1" />
<PackageReference Include="DesignPatterns.Specification.Net.EntityFrameworkCore" />
paket add DesignPatterns.Specification.Net.EntityFrameworkCore --version 26.2.11.1
#r "nuget: DesignPatterns.Specification.Net.EntityFrameworkCore, 26.2.11.1"
#:package DesignPatterns.Specification.Net.EntityFrameworkCore@26.2.11.1
#addin nuget:?package=DesignPatterns.Specification.Net.EntityFrameworkCore&version=26.2.11.1
#tool nuget:?package=DesignPatterns.Specification.Net.EntityFrameworkCore&version=26.2.11.1
DesignPatterns.Specification.Net.EntityFrameworkCore
First-class Entity Framework Core integration for the DesignPatterns.Specification.Net library. Provides EF Core-aware evaluators, DbSet extensions, repository base classes, and optimized query translation for specifications.
๐ฆ Installation
# Install both core and EF Core packages
dotnet add package DesignPatterns.Specification.Net
dotnet add package DesignPatterns.Specification.Net.EntityFrameworkCore
Requirements:
- .NET 9.0 or .NET 10.0
- Entity Framework Core 9.x or 10.x (matched to your target framework)
๐ฏ What This Package Provides
โ EF Core-Specific Evaluators
Translates specification rules into optimized EF Core queries:
- WhereEvaluator: Applies filter criteria to
IQueryable<T> - IncludeEvaluator: Handles
Include()andThenInclude()with optional delegate caching - OrderEvaluator: Translates ordering expressions (
OrderBy,ThenBy) - PaginationEvaluator: Applies
Skip()andTake()for pagination - SearchEvaluator: Converts search patterns to
EF.Functions.Like() - AsNoTrackingEvaluator: Configures change tracking behavior
- AsNoTrackingWithIdentityResolutionEvaluator: No tracking with identity resolution
- AsTrackingEvaluator: Enables change tracking
- AsSplitQueryEvaluator: Splits queries for better performance with multiple includes
- IgnoreQueryFiltersEvaluator: Ignores global query filters
โ DbSet/IQueryable Extensions
Convenient extension methods in the Microsoft.EntityFrameworkCore namespace:
WithSpecification(spec): Apply specification to anyIQueryable<T>ToListAsync(spec, ct): Materialize with post-processingToEnumerableAsync(spec, ct): Lazy enumeration with post-processingSearch(criteria): Apply search patterns usingEF.Functions.Like()
โ Repository Base Classes
Production-ready repository implementations:
- RepositoryBase<T>: Standard repository with DbContext
- ContextFactoryRepositoryBase<TEntity, TContext>: Uses
IDbContextFactory<T>for per-operation contexts - EFRepositoryFactory: Factory pattern for creating repositories
๐ Quick Start
1. Define Your DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Order> Orders => Set<Order>();
}
2. Create a Repository
using DesignPatterns.Specification.Net.EntityFrameworkCore;
public class CustomerRepository : RepositoryBase<Customer>
{
public CustomerRepository(AppDbContext context) : base(context)
{
// NoTracking is set by default in the base class
}
}
3. Use Specifications with Your Repository
public class CustomerService
{
private readonly CustomerRepository _repository;
public CustomerService(CustomerRepository repository)
{
_repository = repository;
}
public async Task<List<Customer>> GetActiveCustomersAsync(CancellationToken ct)
{
var spec = new ActiveCustomersSpec();
return await _repository.ListAsync(spec, ct);
}
public async Task<Customer?> GetCustomerByIdAsync(int id, CancellationToken ct)
{
var spec = new CustomerByIdSpec(id);
return await _repository.FirstOrDefaultAsync(spec, ct);
}
public async Task<int> CountVipCustomersAsync(CancellationToken ct)
{
var spec = new VipCustomersSpec();
return await _repository.CountAsync(spec, ct);
}
}
๐ง Advanced Features
๐ Include Delegate Caching
For specifications with complex include chains, enable caching to reduce reflection overhead:
// Option 1: Use the cached singleton
services.AddSingleton<ISpecificationEvaluator>(
SpecificationEvaluator.Cached
);
// Option 2: Create custom evaluator with caching
var evaluator = new SpecificationEvaluator(cacheEnabled: true);
When to use caching:
- You have many includes and ThenIncludes
- Specifications are executed frequently
- Performance profiling shows Include evaluation as a bottleneck
Performance impact:
- First call: Compiles and caches the delegate (slight overhead)
- Subsequent calls: Reuses cached delegate (significant speedup)
Example Specification with 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)
.Include(o => o.ShippingAddress)
.AsNoTracking();
// With caching enabled, the Include delegates are compiled once
}
}
๐ DbSet Extensions
WithSpecification
Apply a specification to any IQueryable<T>:
using Microsoft.EntityFrameworkCore;
var query = dbContext.Customers
.WithSpecification(new ActiveCustomersSpec());
var customers = await query.ToListAsync(cancellationToken);
WithSpecification (Projection)
Apply a specification with projection:
var query = dbContext.Customers
.WithSpecification(new CustomerNamesSpec()); // Specification<Customer, string>
List<string> names = await query.ToListAsync(cancellationToken);
ToListAsync / ToEnumerableAsync
Materialize results and run post-processing actions:
// Automatically runs PostProcessingAction if defined in spec
var customers = await dbContext.Customers
.ToListAsync(new ActiveCustomersSpec(), cancellationToken);
// Lazy evaluation (IAsyncEnumerable)
await foreach (var customer in dbContext.Customers
.ToEnumerableAsync(new ActiveCustomersSpec(), cancellationToken))
{
Console.WriteLine(customer.Name);
}
๐ Search with EF.Functions.Like
The SearchEvaluator automatically translates search patterns to EF.Functions.Like():
Specification with Search
public class CustomerSearchSpec : Specification<Customer>
{
public CustomerSearchSpec(string searchTerm)
{
Query
.Search(c => c.Name, $"%{searchTerm}%")
.Search(c => c.Email, $"%{searchTerm}%")
.OrderBy(c => c.Name);
}
}
Supported Search Patterns
| Pattern | Description | Example |
|---|---|---|
% |
Matches any sequence of characters | "%smith%" โ Contains "smith" |
_ |
Matches any single character | "_ob" โ "Bob", "Rob", "Job" |
[A-Z] |
Matches any character in range | "[A-C]%" โ Starts with A, B, or C |
[^A-Z] |
Matches any character NOT in range | "[^A-C]%" โ Doesn't start with A, B, C |
In-Memory vs EF Core
// EF Core: Translates to SQL LIKE
var customers = await dbContext.Customers
.ToListAsync(new CustomerSearchSpec("john"), ct);
// SQL: WHERE Name LIKE '%john%' OR Email LIKE '%john%'
// In-Memory: Uses custom Like extension
var customers = new CustomerSearchSpec("john").Evaluate(customerList);
๐๏ธ Repository Patterns
RepositoryBase<T>
Standard repository that uses a single DbContext instance:
public class CustomerRepository : RepositoryBase<Customer>
{
public CustomerRepository(AppDbContext context) : base(context)
{
}
// Add custom methods if needed
public async Task<List<Customer>> GetVipCustomersWithOrdersAsync(CancellationToken ct)
{
return await ListAsync(new VipCustomersSpec(), ct);
}
}
Key features:
- Sets
QueryTrackingBehavior.NoTrackingby default - Implements
IRepositoryBase<T>from core library - Optimizes Count/Any operations (excludes includes/projections)
- Supports all specification features
ContextFactoryRepositoryBase<TEntity, TContext>
For long-lived services (e.g., Blazor components, desktop apps):
public class CustomerRepository : ContextFactoryRepositoryBase<Customer, AppDbContext>
{
public CustomerRepository(IDbContextFactory<AppDbContext> factory)
: base(factory)
{
}
}
Advantages:
- Creates and disposes DbContext per operation
- Safe for long-lived component lifetimes
- Avoids DbContext threading issues
- Ideal for UI scenarios
Setup:
// Program.cs or Startup.cs
services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddScoped<CustomerRepository>();
EFRepositoryFactory
Factory pattern for creating repositories:
public interface ICustomerRepository : IReadRepositoryBase<Customer> { }
public class CustomerRepository : RepositoryBase<Customer>, ICustomerRepository
{
public CustomerRepository(AppDbContext context) : base(context) { }
}
// Register factory
services.AddDbContextFactory<AppDbContext>(/*...*/);
services.AddScoped<IRepositoryFactory<ICustomerRepository>,
EFRepositoryFactory<ICustomerRepository, CustomerRepository, AppDbContext>>();
// Usage
public class CustomerService
{
private readonly IRepositoryFactory<ICustomerRepository> _repoFactory;
public CustomerService(IRepositoryFactory<ICustomerRepository> repoFactory)
{
_repoFactory = repoFactory;
}
public async Task<Customer?> GetCustomerAsync(int id, CancellationToken ct)
{
var repo = _repoFactory.CreateRepository();
return await repo.FirstOrDefaultAsync(new CustomerByIdSpec(id), ct);
}
}
โก Query Optimization Features
AsNoTracking / AsTracking
Control Entity Framework change tracking:
public class ReadOnlyCustomersSpec : Specification<Customer>
{
public ReadOnlyCustomersSpec()
{
Query
.Where(c => c.IsActive)
.AsNoTracking(); // Better performance for read-only queries
}
}
public class EditableCustomersSpec : Specification<Customer>
{
public EditableCustomersSpec()
{
Query
.Where(c => c.IsActive)
.AsTracking(); // Enable change tracking for updates
}
}
AsNoTrackingWithIdentityResolution
When you need no tracking but want identity resolution:
public class CustomersWithIdentityResolutionSpec : Specification<Customer>
{
public CustomersWithIdentityResolutionSpec()
{
Query
.Include(c => c.Orders)
.AsNoTrackingWithIdentityResolution(); // Resolves same entity instances
}
}
Use case: Multiple navigation paths to the same entity need to resolve to a single instance.
AsSplitQuery
For queries with multiple includes, split into separate SQL queries:
public class OrderWithDetailsSpec : Specification<Order>
{
public OrderWithDetailsSpec()
{
Query
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.Include(o => o.ShippingAddress)
.AsSplitQuery(); // Prevents cartesian explosion
}
}
Benefits:
- Avoids cartesian explosion in SQL
- Better performance with multiple collection includes
- Reduces data duplication over the wire
Generated SQL:
-- Query 1: Main entity
SELECT * FROM Orders WHERE ...
-- Query 2: First include
SELECT * FROM Customers WHERE OrderId IN (...)
-- Query 3: Second include
SELECT * FROM OrderItems WHERE OrderId IN (...)
IgnoreQueryFilters
Bypass global query filters when needed:
public class AllCustomersIncludingSoftDeletedSpec : Specification<Customer>
{
public AllCustomersIncludingSoftDeletedSpec()
{
Query
.IgnoreQueryFilters() // Bypass soft-delete filters, etc.
.OrderBy(c => c.Name);
}
}
๐จ Complete Example
Domain Model
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public bool IsActive { get; set; }
public bool IsPremium { get; set; }
public decimal TotalPurchases { get; set; }
public List<Order> Orders { get; set; } = new();
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public DateTime OrderDate { get; set; }
public decimal Total { get; set; }
public List<OrderItem> OrderItems { get; set; } = new();
}
Specifications
// Active customers
public class ActiveCustomersSpec : Specification<Customer>
{
public ActiveCustomersSpec()
{
Query
.Where(c => c.IsActive)
.OrderBy(c => c.Name)
.AsNoTracking();
}
}
// Paginated customer search
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)
.AsNoTracking();
}
}
// Premium customers with orders
public class PremiumCustomersWithOrdersSpec : Specification<Customer>
{
public PremiumCustomersWithOrdersSpec()
{
Query
.Where(c => c.IsPremium && c.IsActive)
.Include(c => c.Orders)
.ThenInclude(o => o.OrderItems)
.OrderByDescending(c => c.TotalPurchases)
.AsNoTracking()
.AsSplitQuery();
}
}
// Customer by ID (single result)
public class CustomerByIdSpec : SingleResultSpecification<Customer>
{
public CustomerByIdSpec(int id)
{
Query
.Where(c => c.Id == id)
.Include(c => c.Orders)
.AsNoTracking();
}
}
// Customer names projection
public class CustomerNamesSpec : Specification<Customer, string>
{
public CustomerNamesSpec()
{
Query
.Where(c => c.IsActive)
.Select(c => c.Name)
.OrderBy(name => name);
}
}
Repository
public class CustomerRepository : RepositoryBase<Customer>
{
public CustomerRepository(AppDbContext context) : base(context)
{
}
}
Service Layer
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<List<Customer>> SearchCustomersAsync(
string searchTerm, int page, int pageSize, CancellationToken ct)
{
var spec = new CustomerSearchSpec(searchTerm, page, pageSize);
return await _repository.ListAsync(spec, ct);
}
public async Task<Customer?> GetCustomerByIdAsync(int id, CancellationToken ct)
{
var spec = new CustomerByIdSpec(id);
return await _repository.FirstOrDefaultAsync(spec, ct);
}
public async Task<int> CountActiveCustomersAsync(CancellationToken ct)
{
return await _repository.CountAsync(new ActiveCustomersSpec(), ct);
}
public async Task<bool> HasPremiumCustomersAsync(CancellationToken ct)
{
return await _repository.AnyAsync(
new Specification<Customer>() { Query = q => q.Where(c => c.IsPremium) },
ct
);
}
public async Task<List<string>> GetActiveCustomerNamesAsync(CancellationToken ct)
{
return await _repository.ListAsync(new CustomerNamesSpec(), ct);
}
}
Dependency Injection Setup
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Register DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Register repositories
builder.Services.AddScoped<CustomerRepository>();
// Register services
builder.Services.AddScoped<CustomerService>();
// Optional: Use cached evaluator for better performance
builder.Services.AddSingleton<ISpecificationEvaluator>(
SpecificationEvaluator.Cached
);
var app = builder.Build();
๐งช Testing
Integration Testing with In-Memory Database
public class CustomerRepositoryTests
{
private AppDbContext CreateInMemoryContext()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new AppDbContext(options);
}
[Fact]
public async Task ListAsync_WithActiveCustomersSpec_ReturnsOnlyActiveCustomers()
{
// Arrange
await using var context = CreateInMemoryContext();
context.Customers.AddRange(
new Customer { Id = 1, Name = "Alice", IsActive = true },
new Customer { Id = 2, Name = "Bob", IsActive = false },
new Customer { Id = 3, Name = "Charlie", IsActive = true }
);
await context.SaveChangesAsync();
var repository = new CustomerRepository(context);
var spec = new ActiveCustomersSpec();
// Act
var result = await repository.ListAsync(spec, CancellationToken.None);
// Assert
Assert.Equal(2, result.Count);
Assert.All(result, c => Assert.True(c.IsActive));
}
[Fact]
public async Task FirstOrDefaultAsync_WithCustomerByIdSpec_ReturnsCorrectCustomer()
{
// Arrange
await using var context = CreateInMemoryContext();
var expectedCustomer = new Customer { Id = 1, Name = "Alice", IsActive = true };
context.Customers.Add(expectedCustomer);
await context.SaveChangesAsync();
var repository = new CustomerRepository(context);
var spec = new CustomerByIdSpec(1);
// Act
var result = await repository.FirstOrDefaultAsync(spec, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Equal("Alice", result.Name);
}
}
๐ Performance Comparison
Include Caching Impact
BenchmarkDotNet v0.13.x, Windows 11
Intel Core i7, 16GB RAM
| Method | Mean | Error | StdDev |
|--------------------------- |---------:|---------:|---------:|
| Default_IncludeEvaluator | 125.3 ฮผs | 2.45 ฮผs | 2.29 ฮผs |
| Cached_IncludeEvaluator | 42.1 ฮผs | 0.83 ฮผs | 0.77 ฮผs |
Recommendation: Use SpecificationEvaluator.Cached for production applications with complex includes.
๐จ Common Pitfalls
โ Don't: Mix AsTracking Flags
// DON'T: Multiple tracking configurations
Query
.AsNoTracking()
.AsTracking(); // โ Last one wins, but confusing
โ Don't: Use Includes for Count/Any
// DON'T: Includes are wasted in count queries
var count = await repository.CountAsync(
new Specification<Customer>()
{
Query = q => q
.Where(c => c.IsActive)
.Include(c => c.Orders) // โ Unnecessary
},
ct
);
The repository automatically excludes includes for Count/Any operations.
โ Do: Use Appropriate Specification Types
// โ
Use SingleResultSpecification for single entity queries
public class CustomerByIdSpec : SingleResultSpecification<Customer>
{
public CustomerByIdSpec(int id)
{
Query.Where(c => c.Id == id);
}
}
โ Do: Use AsSplitQuery for Multiple Includes
// โ
Prevents cartesian explosion
Query
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.Include(o => o.ShippingAddress)
.AsSplitQuery(); // Much better performance
๐ API Reference
SpecificationEvaluator
| Property/Method | Description |
|---|---|
Default |
Singleton with default evaluators (no caching) |
Cached |
Singleton with include delegate caching enabled |
GetQuery<T>(query, spec) |
Apply specification to IQueryable<T> |
GetQuery<T, TResult>(query, spec) |
Apply projection specification |
Repository Methods (IRepositoryBase<T>)
| Method | Description | Optimizations |
|---|---|---|
ListAsync(spec, ct) |
Get all entities | None |
FirstOrDefaultAsync(spec, ct) |
Get first or null | None |
SingleOrDefaultAsync(spec, ct) |
Get single or null | SingleResultSpecification only |
CountAsync(spec, ct) |
Count entities | Excludes includes/projections |
AnyAsync(spec, ct) |
Check existence | Excludes includes/projections |
AddAsync(entity, ct) |
Add entity | Change tracking enabled |
UpdateAsync(entity, ct) |
Update entity | Change tracking enabled |
DeleteAsync(entity, ct) |
Delete entity | Change tracking enabled |
SaveChangesAsync(ct) |
Persist changes | N/A |
DbSet Extension Methods
| Method | Description |
|---|---|
WithSpecification(spec) |
Apply specification to IQueryable |
ToListAsync(spec, ct) |
Materialize with post-processing |
ToEnumerableAsync(spec, ct) |
Lazy enumeration with post-processing |
Search(criteria) |
Apply EF.Functions.Like search |
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Related Packages
- DesignPatterns.Specification.Net - Core specification library
| 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
- DesignPatterns.Specification.Net (>= 26.2.11.1)
- Microsoft.EntityFrameworkCore (>= 10.0.3)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.3)
-
net6.0
- DesignPatterns.Specification.Net (>= 26.2.11.1)
- Microsoft.EntityFrameworkCore (>= 6.0.36)
- Microsoft.EntityFrameworkCore.Relational (>= 6.0.36)
-
net7.0
- DesignPatterns.Specification.Net (>= 26.2.11.1)
- Microsoft.EntityFrameworkCore (>= 7.0.20)
- Microsoft.EntityFrameworkCore.Relational (>= 7.0.20)
-
net8.0
- DesignPatterns.Specification.Net (>= 26.2.11.1)
- Microsoft.EntityFrameworkCore (>= 8.0.24)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.24)
-
net9.0
- DesignPatterns.Specification.Net (>= 26.2.11.1)
- Microsoft.EntityFrameworkCore (>= 9.0.13)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.13)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 26.2.11.1 | 89 | 2/12/2026 |