Softoverse.EntityFrameworkCore.Specification
10.2.0
dotnet add package Softoverse.EntityFrameworkCore.Specification --version 10.2.0
NuGet\Install-Package Softoverse.EntityFrameworkCore.Specification -Version 10.2.0
<PackageReference Include="Softoverse.EntityFrameworkCore.Specification" Version="10.2.0" />
<PackageVersion Include="Softoverse.EntityFrameworkCore.Specification" Version="10.2.0" />
<PackageReference Include="Softoverse.EntityFrameworkCore.Specification" />
paket add Softoverse.EntityFrameworkCore.Specification --version 10.2.0
#r "nuget: Softoverse.EntityFrameworkCore.Specification, 10.2.0"
#:package Softoverse.EntityFrameworkCore.Specification@10.2.0
#addin nuget:?package=Softoverse.EntityFrameworkCore.Specification&version=10.2.0
#tool nuget:?package=Softoverse.EntityFrameworkCore.Specification&version=10.2.0
Specification Pattern for Entity Framework Core
A powerful, lightweight implementation of the Specification Pattern for Entity Framework Core. Encapsulate your query logic into reusable, testable classes and keep your repositories clean.
Table of Contents
- Installation
- Quick Start
- Core Features
- Advanced Usage
- Repository Integration
- CQRS Integration
- Full API Reference
- Best Practices
- Compatibility
- License
Installation
Install via NuGet Package Manager Console:
Install-Package Softoverse.EntityFrameworkCore.Specification
Or via .NET CLI:
dotnet add package Softoverse.EntityFrameworkCore.Specification
Quick Start
1. Define a Specification
public class ActivePremiumUsersSpec : Specification<User>
{
public ActivePremiumUsersSpec()
{
Criteria = u => u.IsActive && u.IsPremium;
// Eager load related data
Include(u => u.Profile);
Include(u => u.Orders)
.ThenInclude(o => o.OrderItems);
// Ordering
OrderByDescending(u => u.CreatedAt);
// Query options
AsNoTracking = true;
}
}
2. Apply it in your Repository
public async Task<List<User>> GetPremiumUsersAsync(ISpecification<User> spec)
{
// ApplySpecification is an extension method for IQueryable<T>, DbSet<T>, and DbContext
return await _context.Users.ApplySpecification(spec).ToListAsync();
}
Core Features
Filtering (Criteria)
The Criteria property defines the Where clause of your query.
var spec = new Specification<User>
{
Criteria = u => u.Age > 18
};
You can also pass multiple expressions to the constructor:
var spec = new Specification<User>(
[u => u.IsActive, u => u.Email.Contains("@gmail.com")],
CombineType.And
);
Includes (Eager Loading)
The library provides a fluent API for Include and ThenInclude, supporting both collections and single navigation properties.
Simple & Nested Includes
spec.Include(u => u.Orders)
.ThenInclude(o => o.Items);
String-based Includes
spec.IncludeString("Orders.Items");
Filtered Includes (EF Core 5+)
You can apply filters directly within the Include:
spec.Include(u => u.Orders.Where(o => o.Status == OrderStatus.Shipped));
Ordering
The library supports fluent OrderBy and ThenBy methods for multi-level sorting, providing a chainable API similar to LINQ.
Simple Ordering
// Ascending order
spec.OrderBy(u => u.LastName);
// SQL: ORDER BY [u].[LastName]
// Descending order
spec.OrderByDescending(u => u.CreatedAt);
// SQL: ORDER BY [u].[CreatedAt] DESC
Multi-Level Ordering with ThenBy
Chain multiple ordering levels for complex sorting scenarios:
// Order by multiple fields
spec.OrderBy(u => u.IsActive)
.ThenByDescending(u => u.CreatedAt)
.ThenBy(u => u.LastName);
// SQL: ORDER BY [u].[IsActive], [u].[CreatedAt] DESC, [u].[LastName]
Combining with Includes
OrderBy can be seamlessly chained with Include operations:
// Chain OrderBy after Include
spec.Include(u => u.Orders)
.ThenInclude(o => o.Items)
.OrderByDescending(u => u.CreatedAt);
// Or chain Include after OrderBy
spec.OrderBy(u => u.Name)
.Include(u => u.Profile);
Real-World Examples
// E-commerce: Sort products by category, then rating, then price
spec.Criteria = p => p.InStock;
spec.OrderBy(p => p.CategoryId)
.ThenByDescending(p => p.AverageRating)
.ThenBy(p => p.Price);
// User management: Sort by department, last name, first name
spec.OrderBy(u => u.Department)
.ThenBy(u => u.LastName)
.ThenBy(u => u.FirstName);
// Blog: Featured posts first, then by date
spec.OrderByDescending(p => p.IsFeatured)
.ThenByDescending(p => p.PublishedDate);
Important Notes
- Database-level sorting: All ordering is applied at the SQL level, not in memory
- Performance: Add database indexes on frequently ordered columns for optimal performance
- OrderBy clears previous ordering: Calling
OrderBy()orOrderByDescending()clears any previous ordering - Use ThenBy for secondary sorts: Always use
ThenBy()/ThenByDescending()for additional sorting levels
Migration Note: The old AddOrderBy() and AddOrderByDescending() methods are still supported but marked as obsolete. Migrate to the new fluent API:
// ❌ Old (Obsolete)
spec.AddOrderBy(u => u.Name);
// ✅ New (Recommended)
spec.OrderBy(u => u.Name);
Projections
Use SetProjection to select only the fields you need.
spec.SetProjection(u => new { u.Id, u.FullName });
No-Tracking & Split Queries
Optimize performance for read-only or complex queries.
spec.AsNoTracking = true;
spec.AsSplitQuery = true;
Advanced Usage
Dynamic Expression Generation
The ToConditionalExpression static methods allow you to generate expressions dynamically based on values and operators.
// Generates: u => u.Age > 25
var ageExp = Specification<User>.ToConditionalExpression(
u => u.Age,
25,
CompareOperation.GreaterThan
);
// Generates: u => u.Name == "John"
var nameExp = Specification<User>.ToConditionalExpression(
u => u.Name,
"John",
EqualOperation.Equal
);
Smart Filtering (ToConditionalExpressionInternal)
The ToConditionalExpression method internally uses ToConditionalExpressionInternal to provide "Smart Filtering" capabilities. This allows you to generate complex expressions using simple string patterns, which is especially useful for handling query string parameters from an API.
Supported Operators
| Operator | Description | Example |
|---|---|---|
eq / ne |
Equal / Not Equal | eq:John, ne:John |
gt / gte |
Greater Than / Or Equal | gt:18, gte:18 |
lt / lte |
Less Than / Or Equal | lt:18, lte:18 |
eqci |
Equal (Case-Insensitive) | eqci:john |
like / likeci |
Contains / Case-Insensitive | like:jo, likeci:JO |
range |
Range (Inclusive) | range:18,30 |
in / nin |
In List / Not In List | in:1,2,3, nin:1,2,3 |
inci / ninci |
In/Not In List (Case-Insensitive) | inci:a,b, ninci:c,d |
inlike / ninlike |
In/Not In List (Pattern Match) | inlike:a,b, ninlike:x,y |
inlikeci / ninlikeci |
Case-Insensitive Pattern Match | inlikeci:A,B |
Usage Example
// These strings would typically come from your API query parameters
string ageFilter = "gt:18";
string nameFilter = "like:John";
string categoryFilter = "in:Electronics,Books";
var spec = new Specification<User>();
spec.Criteria = Specification<User>.ToConditionalExpression(u => u.Age, ageFilter);
spec.Criteria = spec.Criteria.And(Specification<User>.ToConditionalExpression(u => u.Name, nameFilter));
spec.Criteria = spec.Criteria.And(Specification<User>.ToConditionalExpression(u => u.Category, categoryFilter));
Bulk Updates (ExecuteUpdate)
Integration with EF Core's ExecuteUpdate for efficient bulk operations.
// Option 1: Using property selectors
var spec = new Specification<User>
{
Criteria = u => u.Id == 1,
ExecuteUpdateProperties = [u => u.Name, u => u.Email]
};
// After updating properties on the user object:
await context.Users.ExecuteUpdateAsync(spec, userObject);
// Option 2: Using fluent SetProperty
spec.SetExecuteUpdateExpression(setters => setters
.SetProperty(u => u.IsActive, true)
.SetProperty(u => u.LastLogin, DateTime.UtcNow));
Expression Combiner
The ExpressionBuilder provides extension methods to combine expressions manually.
Expression<Func<User, bool>> criteria1 = u => u.IsActive;
Expression<Func<User, bool>> criteria2 = u => u.Age > 18;
var combined = criteria1.And(criteria2);
var either = criteria1.Or(criteria2);
Repository Integration
A typical generic repository implementation:
public class Repository<T> where T : class
{
private readonly DbContext _context;
public Repository(DbContext context) => _context = context;
public async Task<List<T>> ListAsync(ISpecification<T> spec)
{
return await _context.Set<T>()
.ApplySpecification(spec)
.ToListAsync();
}
public async Task<T?> GetAsync(ISpecification<T> spec)
{
return await _context.Set<T>()
.ApplySpecification(spec)
.FirstOrDefaultAsync();
}
}
CQRS Integration
For projects using the CQRS pattern (e.g., with MediatR), you can use the ISpecificationRequest<TEntity> interface to bridge your queries and specifications.
Defining a Specification Request
Inherit your query classes from ISpecificationRequest<TEntity> to standardize how specifications are retrieved from requests.
public class GetActiveUsersQuery : ISpecificationRequest<User>
{
public string SearchTerm { get; set; }
public ISpecification<User> GetSpecification(bool asNoTracking = false, bool asSplitQuery = false)
{
var spec = new Specification<User>
{
AsNoTracking = asNoTracking,
AsSplitQuery = asSplitQuery
};
if (!string.IsNullOrWhiteSpace(SearchTerm))
{
spec.Criteria = u => u.IsActive && u.Name.Contains(SearchTerm);
}
else
{
spec.Criteria = u => u.IsActive;
}
spec.Include(u => u.Profile);
spec.OrderBy(u => u.Name);
return spec;
}
}
Usage in a Query Handler
In your handler, simply call GetSpecification() to obtain the specification and apply it to your IQueryable.
public class GetActiveUsersHandler
{
private readonly ApplicationDbContext _context;
public GetActiveUsersHandler(ApplicationDbContext context) => _context = context;
public async Task<List<User>> Handle(GetActiveUsersQuery request)
{
var spec = request.GetSpecification(asNoTracking: true);
return await _context.Users
.ApplySpecification(spec)
.ToListAsync();
}
}
Full API Reference
Interfaces
ISpecification<TEntity>
The primary interface for defining specifications.
Criteria: The filter expression.AsNoTracking: Boolean for tracking behavior.AsSplitQuery: Boolean for query splitting behavior.OrderByExpression: (Obsolete) Primary sort expression. UseOrderBy()method instead.OrderByDescendingExpression: (Obsolete) Primary descending sort expression. UseOrderByDescending()method instead.OrderByExpressions: List of ordering expressions with direction indicators for multi-level sorting.ProjectionExpression: Expression for projecting to a different type.ExecuteUpdateExpression: Action for bulk updates usingUpdateSettersBuilder.ExecuteUpdateProperties: List of property selectors for object-based bulk updates.
ISpecificationRequest<TEntity>
Used for bridging Query objects (CQRS) with Specifications.
GetSpecification(bool asNoTracking, bool asSplitQuery): Returns anISpecification<TEntity>.
ISpecificationForPrimaryKey
Base interface for specifications that might use a primary key.
PrimaryKey: The primary key value.
IIncludableSpecification<TEntity, TProperty>
Enables fluent ThenInclude chaining.
ThenInclude<TNextProperty>(expression): Chains a sub-property load.
IOrderableSpecification<TEntity, TProperty>
Enables fluent ThenBy and ThenByDescending chaining for multi-level sorting.
ThenBy<TNextProperty>(keySelector): Chains an ascending sort on a property.ThenByDescending<TNextProperty>(keySelector): Chains a descending sort on a property.
Classes
Specification<TEntity>
The base implementation of ISpecification<TEntity>.
- Constructors:
Specification(): Empty specification.Specification(List<Expression>, CombineType): Combines multiple expressions.Specification(object primaryKey): Specification for a specific record.
- Methods:
Include(expression): Adds an eager load.IncludeString(path): Adds a string-based eager load.OrderBy(keySelector): Sets ascending order and returns anIOrderableSpecificationfor chaining.OrderByDescending(keySelector): Sets descending order and returns anIOrderableSpecificationfor chaining.AddOrderBy(expression): (Obsolete) Sets the ascending order. UseOrderBy()instead.AddOrderByDescending(expression): (Obsolete) Sets the descending order. UseOrderByDescending()instead.SetProjection(expression): Sets the selection projection.SetExecuteUpdateExpression(action): Sets the fluent bulk update logic.AddExecuteUpdateProperties(expression): Adds a property for bulk update.ToConditionalExpression(...): Static helper to generate expressions from values/operators.
ExpressionGenerator<TEntity> (Static)
Helper for generating complex expressions dynamically.
BuildUpdateExpression(properties, model): Generates anExecuteUpdateexpression from an object and selected properties.BuildUpdateExpression(propertyUpdates): Generates anExecuteUpdateexpression from a dictionary of property paths and values.
Enums
CombineType
Used when combining multiple expressions in the constructor.
And: All criteria must be met.Or: At least one criteria must be met.
EqualOperation / CompareOperation
Used in ToConditionalExpression to define comparison logic.
Equal,NotEqual,GreaterThan,GreaterThanOrEqual,LessThan,LessThanOrEqual.
Extension Methods
SpecificationEvaluator
ApplySpecification(queryable, specification): Applies all specification logic to anIQueryable.ApplySpecification(dbSet, specification): Helper forDbSet.ApplySpecification(dbContext, specification): Helper forDbContext.
ExpressionBuilder
And(left, right): Combines two expressions with AND.Or(left, right): Combines two expressions with OR.Not(expression): Negates an expression.When(expression, condition): Conditionally applies an expression.CombineWithAnd(expressions): Aggregates multiple expressions with AND.CombineWithOr(expressions): Aggregates multiple expressions with OR.
Best Practices
- Encapsulation: Put complex query logic inside named Specification classes instead of building them in your services or repositories.
- Reusability: Use small, focused specifications and combine them if necessary.
- Read-Only: Always use
AsNoTracking = truefor specifications intended for read-only display to improve performance.
Compatibility
- .NET 6.0+
- Entity Framework Core 6.0+
License
Licensed under the Apache License, Version 2.0. See LICENSE for details.
Support
For issues, please open a GitHub issue in the 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
- Microsoft.EntityFrameworkCore (>= 10.0.1)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.1)
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 |
|---|---|---|
| 10.2.0 | 82 | 1/20/2026 |
| 10.0.0 | 102 | 12/31/2025 |
| 9.0.0 | 88 | 12/31/2025 |
| 2.5.1 | 135 | 9/27/2025 |
| 2.5.0 | 245 | 4/11/2025 |
| 2.4.2 | 146 | 2/16/2025 |
| 2.4.1 | 142 | 2/16/2025 |
| 2.4.0 | 163 | 2/16/2025 |
| 2.3.3 | 143 | 2/16/2025 |
| 2.3.2 | 146 | 2/16/2025 |
| 2.3.1 | 141 | 2/16/2025 |
| 2.3.0 | 151 | 2/16/2025 |
| 2.2.0 | 168 | 2/10/2025 |
| 2.1.1 | 166 | 2/10/2025 |
| 2.1.0 | 171 | 2/10/2025 |
| 2.0.4 | 160 | 2/9/2025 |
| 2.0.3 | 152 | 2/9/2025 |
| 2.0.2 | 155 | 2/9/2025 |
| 2.0.1 | 142 | 2/9/2025 |
| 2.0.0 | 156 | 2/9/2025 |
| 1.3.1 | 159 | 2/9/2025 |
| 1.3.0 | 148 | 2/9/2025 |
| 1.2.1 | 137 | 2/9/2025 |
| 1.2.0 | 153 | 2/8/2025 |
| 1.1.0 | 156 | 2/8/2025 |
| 1.0.0 | 162 | 2/8/2025 |