DKNet.EfCore.Specifications
10.0.2
See the version list below for details.
dotnet add package DKNet.EfCore.Specifications --version 10.0.2
NuGet\Install-Package DKNet.EfCore.Specifications -Version 10.0.2
<PackageReference Include="DKNet.EfCore.Specifications" Version="10.0.2" />
<PackageVersion Include="DKNet.EfCore.Specifications" Version="10.0.2" />
<PackageReference Include="DKNet.EfCore.Specifications" />
paket add DKNet.EfCore.Specifications --version 10.0.2
#r "nuget: DKNet.EfCore.Specifications, 10.0.2"
#:package DKNet.EfCore.Specifications@10.0.2
#addin nuget:?package=DKNet.EfCore.Specifications&version=10.0.2
#tool nuget:?package=DKNet.EfCore.Specifications&version=10.0.2
DKNet.EfCore.Specifications
A powerful and flexible specification pattern implementation for Entity Framework Core with dynamic LINQ query support powered by System.Linq.Dynamic.Core and LinqKit.
Features
- 🎯 Specification Pattern - Build reusable, composable query logic with
Specification<TEntity> - ⚡ Dynamic LINQ Queries - Runtime query construction using System.Linq.Dynamic.Core
- 🔗 Fluent API - Chainable
DynamicAnd()andDynamicOr()extension methods - 🛠️ Type-Safe Operations - Strongly-typed filter operations with automatic type handling
- 📊 LinqKit Integration - Seamlessly combine static and dynamic predicates with
PredicateBuilder - 🔄 Property Name Normalization - Supports camelCase, snake_case, kebab-case, and PascalCase
- 🎭 Model Projections -
ModelSpecification<TEntity, TModel>for DTO/projection scenarios - 🌊 Async Streaming - Page-based async enumeration for large result sets
- ✅ Null-Safe - Proper handling of nullable properties and null values in SQL
- 🔍 Enum Validation - Automatic enum type validation and conversion
Installation
dotnet add package DKNet.EfCore.Specifications
Quick Start
1. Basic Specification Usage
public class ActivePersonsSpec : Specification<Person>
{
public ActivePersonsSpec()
{
// Static filter
WithFilter(p => p.IsActive && !p.IsDeleted);
// Include related entities
AddInclude(p => p.Address);
// Add ordering
AddOrderBy(p => p.Name);
}
}
// Usage with repository
var spec = new ActivePersonsSpec();
var persons = await repository.ToListAsync(spec);
2. Dynamic Predicates with DynamicAnd/DynamicOr
Build dynamic queries at runtime using fluent extension methods:
public class PersonSearchSpec : Specification<Person>
{
public PersonSearchSpec(int? minAge, string? nameContains, string? department)
{
// Start with base predicate
var predicate = PredicateBuilder.New<Person>(p => !p.IsDeleted);
// Add dynamic filters conditionally
if (minAge.HasValue)
predicate = predicate.DynamicAnd("Age", DynamicOperations.GreaterThanOrEqual, minAge);
if (!string.IsNullOrEmpty(nameContains))
predicate = predicate.DynamicAnd("Name", DynamicOperations.Contains, nameContains);
if (!string.IsNullOrEmpty(department))
predicate = predicate.DynamicOr("Department.Name", DynamicOperations.Equal, department);
WithFilter(predicate);
}
}
// Usage
var spec = new PersonSearchSpec(minAge: 18, nameContains: "John", department: null);
var results = await repository.ToListAsync(spec);
3. Property Name Normalization
Property names are automatically normalized to PascalCase, supporting multiple naming conventions:
var predicate = PredicateBuilder.New<Employee>()
// All of these are equivalent and normalize to "FirstName"
.DynamicAnd("firstName", DynamicOperations.Equal, "John") // camelCase
.DynamicAnd("first_name", DynamicOperations.Equal, "John") // snake_case
.DynamicAnd("first-name", DynamicOperations.Equal, "John") // kebab-case
.DynamicAnd("FirstName", DynamicOperations.Equal, "John"); // PascalCase
// Nested properties also supported
predicate = predicate.DynamicAnd("address.city", DynamicOperations.Equal, "New York");
// Normalizes to: Address.City
4. Model Specifications for Projections
Use ModelSpecification<TEntity, TModel> for scenarios involving DTOs or projections:
public class EmployeeListSpec : ModelSpecification<Employee, EmployeeDto>
{
public EmployeeListSpec(string? departmentFilter)
{
var predicate = PredicateBuilder.New<Employee>(e => e.IsActive);
if (!string.IsNullOrEmpty(departmentFilter))
predicate = predicate.DynamicAnd("Department.Name", DynamicOperations.Equal, departmentFilter);
WithFilter(predicate);
AddOrderBy(e => e.LastName);
AddOrderBy(e => e.FirstName);
}
}
// Usage with automatic projection (using Mapster or AutoMapper)
var spec = new EmployeeListSpec("Engineering");
var dtos = await repository.ToListAsync<Employee, EmployeeDto>(spec);
Available Operations
The DynamicOperations enum supports the following operations:
| Operation | Description | Example Usage | Auto-Conversion |
|---|---|---|---|
Equal |
Equality comparison (==) | .DynamicAnd("Age", Equal, 25) |
- |
NotEqual |
Inequality comparison (!=) | .DynamicAnd("Status", NotEqual, "Inactive") |
- |
GreaterThan |
Greater than (>) | .DynamicAnd("Salary", GreaterThan, 50000) |
- |
GreaterThanOrEqual |
Greater than or equal (>=) | .DynamicAnd("Age", GreaterThanOrEqual, 18) |
- |
LessThan |
Less than (<) | .DynamicAnd("Price", LessThan, 100) |
- |
LessThanOrEqual |
Less than or equal (<=) | .DynamicAnd("Score", LessThanOrEqual, 100) |
- |
Contains |
String contains | .DynamicAnd("Name", Contains, "Smith") |
→ Equal* |
NotContains |
String does not contain | .DynamicAnd("Email", NotContains, "spam") |
→ NotEqual* |
StartsWith |
String starts with | .DynamicAnd("Phone", StartsWith, "+1") |
→ Equal* |
EndsWith |
String ends with | .DynamicAnd("Email", EndsWith, "@company.com") |
→ Equal* |
* Auto-Conversion: For non-string types (int, enum, bool, double, etc.), string operations are automatically converted to equality operations.
Null Value Handling
The library properly handles null values in SQL queries:
// NULL equality check
predicate = predicate.DynamicAnd("MiddleName", DynamicOperations.Equal, null);
// SQL: WHERE [MiddleName] IS NULL
// NULL inequality check
predicate = predicate.DynamicAnd("MiddleName", DynamicOperations.NotEqual, null);
// SQL: WHERE [MiddleName] IS NOT NULL
Enum Validation
Enum properties are automatically validated. Only Equal and NotEqual operations are supported for enums:
public enum Status { Active, Inactive, Pending }
// Valid enum operations
predicate = predicate.DynamicAnd("Status", DynamicOperations.Equal, Status.Active);
predicate = predicate.DynamicAnd("Status", DynamicOperations.NotEqual, Status.Inactive);
// Invalid enum values are ignored (predicate remains unchanged)
predicate = predicate.DynamicAnd("Status", DynamicOperations.Equal, "InvalidValue");
// Contains/StartsWith/EndsWith are auto-converted to Equal for enums
predicate = predicate.DynamicAnd("Status", DynamicOperations.Contains, Status.Active);
// Automatically becomes: Equal operation
Advanced Scenarios
Multi-Field Search with OR Logic
public class ProductSearchSpec : Specification<Product>
{
public ProductSearchSpec(string searchTerm)
{
var predicate = PredicateBuilder.New<Product>(true);
// Search across multiple fields using OR
predicate = predicate
.DynamicOr("Name", DynamicOperations.Contains, searchTerm)
.DynamicOr("Description", DynamicOperations.Contains, searchTerm)
.DynamicOr("SKU", DynamicOperations.Equal, searchTerm);
WithFilter(predicate);
}
}
Complex AND/OR Combinations
public class EmployeeFilterSpec : Specification<Employee>
{
public EmployeeFilterSpec(string? department, decimal? minSalary, bool includeInactive)
{
var predicate = PredicateBuilder.New<Employee>(true);
// Department filter (OR across multiple departments)
if (!string.IsNullOrEmpty(department))
{
var deptPredicate = PredicateBuilder.New<Employee>(false);
foreach (var dept in department.Split(','))
{
deptPredicate = deptPredicate.DynamicOr("Department.Name", DynamicOperations.Equal, dept.Trim());
}
predicate = predicate.And(deptPredicate);
}
// Salary filter (AND)
if (minSalary.HasValue)
predicate = predicate.DynamicAnd("Salary", DynamicOperations.GreaterThanOrEqual, minSalary);
// Active status (AND)
if (!includeInactive)
predicate = predicate.DynamicAnd("IsActive", DynamicOperations.Equal, true);
WithFilter(predicate);
}
}
API Controller with Dynamic Queries
[HttpGet]
public async Task<IActionResult> SearchEmployees(
[FromQuery] string? search,
[FromQuery] string? department,
[FromQuery] string? orderBy,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var spec = new ModelSpecification<Employee, EmployeeDto>();
var predicate = PredicateBuilder.New<Employee>(e => !e.IsDeleted);
// Dynamic search
if (!string.IsNullOrEmpty(search))
{
predicate = predicate
.DynamicOr("FirstName", DynamicOperations.Contains, search)
.DynamicOr("LastName", DynamicOperations.Contains, search)
.DynamicOr("Email", DynamicOperations.Contains, search);
}
// Department filter
if (!string.IsNullOrEmpty(department))
predicate = predicate.DynamicAnd("Department.Name", DynamicOperations.Equal, department);
spec.WithFilter(predicate);
// Dynamic ordering (supports camelCase, snake_case, etc.)
if (!string.IsNullOrEmpty(orderBy))
spec.AddOrderBy(orderBy, ListSortDirection.Ascending);
else
spec.AddOrderBy(e => e.LastName);
var pagedResults = await _repository.ToPagedListAsync<Employee, EmployeeDto>(spec, page, pageSize);
return Ok(new
{
items = pagedResults,
totalCount = pagedResults.TotalItemCount,
pageCount = pagedResults.PageCount
});
}
Nested Property Filtering
var spec = new Specification<Order>();
var predicate = PredicateBuilder.New<Order>(true)
// Nested property access
.DynamicAnd("Customer.Address.City", DynamicOperations.Equal, "New York")
.DynamicAnd("Customer.Address.ZipCode", DynamicOperations.StartsWith, "100")
// Multi-level navigation
.DynamicAnd("OrderItems.Product.Category.Name", DynamicOperations.Equal, "Electronics")
// Combination with static predicates
.And(o => o.OrderDate >= DateTime.Today.AddDays(-30));
spec.WithFilter(predicate);
Async Streaming for Large Result Sets
var spec = new ModelSpecification<Product, ProductDto>();
spec.WithFilter(p => p.IsActive);
spec.AddOrderBy(p => p.Id);
// Stream results page-by-page
await foreach (var product in _repository.PageAsync<Product, ProductDto>(spec))
{
// Process each product without loading entire result set into memory
await ProcessProductAsync(product);
}
Repository Extensions
The library provides rich extension methods for IRepositorySpec:
Entity-Only Operations
// Count
int count = await repository.CountAsync(spec);
// Any
bool hasResults = await repository.AnyAsync(spec);
// First
Employee employee = await repository.FirstAsync(spec);
// First or default
Employee? maybeEmployee = await repository.FirstOrDefaultAsync(spec);
// List
IList<Employee> employees = await repository.ToListAsync(spec);
// Paged list
IPagedList<Employee> pagedEmployees = await repository.ToPagedListAsync(spec, pageNumber: 1, pageSize: 20);
// Async enumeration
await foreach (var emp in repository.PageAsync(spec))
{
// Process
}
// Get raw query (useful for debugging)
IQueryable<Employee> query = repository.Query(spec);
string sql = query.ToQueryString();
Model Projection Operations
// First or default with projection
EmployeeDto? dto = await repository.FirstOrDefaultAsync<Employee, EmployeeDto>(spec);
// List with projection
IList<EmployeeDto> dtos = await repository.ToListAsync<Employee, EmployeeDto>(spec);
// Paged list with projection
IPagedList<EmployeeDto> pagedDtos = await repository.ToPagedListAsync<Employee, EmployeeDto>(spec, 1, 20);
// Async enumeration with projection
await foreach (var dto in repository.PageAsync<Employee, EmployeeDto>(spec))
{
// Process
}
// Get raw projected query
IQueryable<EmployeeDto> query = repository.Query<Employee, EmployeeDto>(spec);
Best Practices
- Reusability - Create named specification classes for common query patterns
- Composition - Build complex queries by combining simple predicates using
And()andOr() - Type Safety - The library automatically handles type conversions and validates enum values
- Null Safety - Null values are handled correctly (translates to
IS NULL/IS NOT NULLin SQL) - Property Naming - Use any naming convention you prefer; it will be normalized to PascalCase
- Performance - Use
ModelSpecification<TEntity, TModel>with projections to reduce data transfer - Large Result Sets - Use
PageAsync()for streaming orToPagedListAsync()for pagination - Debugging - Use
.ToQueryString()on the query to see generated SQL
Type-Specific Behavior
String Properties
- All operations supported:
Equal,NotEqual,Contains,NotContains,StartsWith,EndsWith, comparison operators - Null values handled correctly with
IS NULL/IS NOT NULL
Numeric Properties (int, long, decimal, double, etc.)
- Comparison operations:
Equal,NotEqual,GreaterThan,GreaterThanOrEqual,LessThan,LessThanOrEqual - String operations (
Contains, etc.) auto-converted toEqual
Enum Properties
- Only
EqualandNotEqualsupported - Invalid enum values are ignored (no exception thrown)
- Automatic enum validation and conversion
- String operations auto-converted to
Equal
Boolean Properties
EqualandNotEqualoperations- String operations auto-converted to
Equal
Nullable Types
- Full support for nullable reference types and nullable value types
- Null comparisons translate to proper SQL (
IS NULL/IS NOT NULL)
Requirements
- .NET 9.0 or higher
- Entity Framework Core 9.0 or higher
- LinqKit.Microsoft.EntityFrameworkCore 8.1.0+
- System.Linq.Dynamic.Core 1.4.0+
- X.PagedList 9.2.0+ (for pagination)
License
Licensed under the MIT License. See LICENSE in the project root for license information.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
For issues, questions, or feature requests, please open an issue on the GitHub repository.
| 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
- DKNet.EfCore.Extensions (>= 10.0.2)
- LinqKit.Microsoft.EntityFrameworkCore (>= 10.0.9)
- Mapster (>= 7.4.0)
- Microsoft.EntityFrameworkCore (>= 10.0.0)
- Microsoft.EntityFrameworkCore.DynamicLinq (>= 10.7.0)
- System.Linq.Dynamic.Core (>= 1.7.0)
- X.PagedList.EF (>= 10.5.9)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on DKNet.EfCore.Specifications:
| Package | Downloads |
|---|---|
|
DKNet.EfCore.Repos
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.3 | 129 | 11/21/2025 |
| 10.0.2 | 175 | 11/20/2025 |
| 9.5.40 | 169 | 11/19/2025 |
| 9.5.39 | 319 | 11/13/2025 |
| 9.5.38 | 228 | 11/6/2025 |
| 9.5.37 | 206 | 11/5/2025 |
| 9.5.36 | 209 | 11/5/2025 |
| 9.5.35 | 207 | 11/4/2025 |
| 9.5.34 | 200 | 11/4/2025 |
| 9.5.33 | 208 | 11/3/2025 |
| 9.5.32 | 207 | 11/3/2025 |
| 9.5.31 | 179 | 10/31/2025 |
| 9.5.30 | 201 | 10/31/2025 |
| 9.5.29 | 213 | 10/30/2025 |
| 9.5.28 | 203 | 10/27/2025 |
| 9.5.27 | 217 | 10/27/2025 |
| 9.5.26 | 203 | 10/27/2025 |
| 9.5.25 | 190 | 10/26/2025 |
| 9.5.24 | 95 | 10/25/2025 |
| 9.5.23 | 88 | 10/25/2025 |
| 9.5.22 | 97 | 10/25/2025 |
| 9.5.21 | 157 | 10/24/2025 |
| 9.5.20 | 174 | 10/23/2025 |
| 9.5.19 | 157 | 10/23/2025 |
| 9.5.18 | 161 | 10/22/2025 |
| 9.5.17 | 190 | 10/17/2025 |
| 9.5.16 | 136 | 10/17/2025 |
| 9.5.15 | 176 | 10/15/2025 |
| 9.5.14 | 175 | 10/14/2025 |
| 9.5.13 | 161 | 10/14/2025 |
| 9.5.12 | 161 | 10/14/2025 |
| 9.5.11 | 165 | 10/14/2025 |
| 9.5.10 | 184 | 10/14/2025 |
| 9.5.9 | 161 | 10/13/2025 |
| 9.5.8 | 102 | 10/11/2025 |
| 9.5.7 | 114 | 10/10/2025 |
| 9.5.6 | 117 | 10/10/2025 |
| 9.5.5 | 121 | 10/10/2025 |
| 9.5.4 | 131 | 10/10/2025 |
| 9.5.3 | 195 | 10/8/2025 |
| 9.5.2 | 158 | 10/8/2025 |
| 9.5.1 | 178 | 10/7/2025 |
| 9.0.42 | 165 | 10/6/2025 |
| 9.0.41 | 173 | 10/2/2025 |
| 9.0.40 | 140 | 9/27/2025 |
| 9.0.39 | 148 | 9/26/2025 |
| 9.0.38 | 175 | 9/24/2025 |
| 9.0.37 | 156 | 9/23/2025 |
| 9.0.36 | 187 | 9/23/2025 |
| 9.0.35 | 171 | 9/23/2025 |
| 9.0.34 | 175 | 9/23/2025 |
| 9.0.33 | 157 | 9/21/2025 |
| 9.0.32 | 156 | 9/21/2025 |
| 9.0.31 | 288 | 9/19/2025 |
| 9.0.30 | 291 | 9/18/2025 |
| 9.0.29 | 281 | 9/18/2025 |
| 9.0.28 | 303 | 9/17/2025 |
| 9.0.27 | 295 | 9/17/2025 |
| 9.0.26 | 300 | 9/16/2025 |
| 9.0.25 | 243 | 9/15/2025 |
| 9.0.24 | 250 | 9/15/2025 |
| 0.0.1 | 171 | 11/19/2025 |