EricksonLopez.Specification.Oracle
1.0.0
dotnet add package EricksonLopez.Specification.Oracle --version 1.0.0
NuGet\Install-Package EricksonLopez.Specification.Oracle -Version 1.0.0
<PackageReference Include="EricksonLopez.Specification.Oracle" Version="1.0.0" />
<PackageVersion Include="EricksonLopez.Specification.Oracle" Version="1.0.0" />
<PackageReference Include="EricksonLopez.Specification.Oracle" />
paket add EricksonLopez.Specification.Oracle --version 1.0.0
#r "nuget: EricksonLopez.Specification.Oracle, 1.0.0"
#:package EricksonLopez.Specification.Oracle@1.0.0
#addin nuget:?package=EricksonLopez.Specification.Oracle&version=1.0.0
#tool nuget:?package=EricksonLopez.Specification.Oracle&version=1.0.0
EricksonLopez.Specification
High-performance, composable, NativeAOT-first Specification Pattern and SQL AST translation ecosystem for modern .NET.
EricksonLopez.Specification is an enterprise-grade, AOT-first, provider-agnostic Specification Pattern and SQL AST translation engine targeting .NET 8, .NET 9, and .NET 10. Engineered from the ground up for strict Domain-Driven Design (DDD) layering, it eliminates runtime code generation crashes under NativeAOT via a dedicated in-memory AST interpreter (ExpressionInterpreter), translates LINQ expressions directly into parameterized SQL across 6 database dialects without ORM dependencies for Dapper, and enforces immutability, thread-safety, and domain purity at compile time through 11 custom Roslyn analyzers.
Table of Contents
- What Problem It Solves
- Key Features
- Ecosystem
- Documentation
- Installation
- Quick Start
- Core Use Cases
- Use Case 1: Clean Architecture / CQRS Query Handler with Repository Pattern
- Use Case 2: Multi-Step Domain Rule Composition with Short-Circuiting
- Use Case 3: Keyset / Cursor Pagination on High-Volume Datasets
- Use Case 4: NativeAOT Microservices with Interpreted Validation
- Use Case 5: Multi-Dialect SQL Generation for Dapper & Raw ADO.NET
- Use Case 6: Functional Result Queries with
EricksonLopez.Result
- Configuration & Integrations
- Testing & Quality
- Performance Benchmarks
- Compatibility & Technical Matrix
- Architecture & Design Principles
- Best Practices & Anti-Patterns
- Troubleshooting & Common Pitfalls
- Part of the EricksonLopez Ecosystem
- Contributing
- License
๐ฏ What Problem It Solves
The Pain Points of Traditional Specification Libraries
- NativeAOT Runtime Failures & Dynamic Code Crashes: Traditional libraries rely heavily on
Expression.Compile()or runtime IL generation (System.Reflection.Emit) for in-memory predicate evaluation. Under NativeAOT or trimmed deployments, these dynamic paths throw runtime exceptions or fail trimming verification. - ORM Coupling & Architectural Leakage in Domain Layers: Legacy implementations bundle ORM concepts (
Include,ThenInclude,AsNoTracking, split queries) directly into domain specification base classes. This violates Clean Architecture principles, pollutes domain entities with infrastructure details, and makes domain logic unusable outside Entity Framework. - Lack of Provider-Agnostic SQL Generation for Micro-ORMs: Developers wanting high performance with Dapper or raw ADO.NET are forced to manually write string-based SQL queries, discarding domain specifications and introducing SQL injection vulnerabilities and maintainability nightmares.
- Mutable State &
Expression.InvokeProvider Failures: Combining expressions usingExpression.Invokebreaks query translation in EF Core, Cosmos DB, and LINQ providers, requiring fragile third-party extensions like LinqKit that fail NativeAOT trimming.
How EricksonLopez.Specification Solves This
- Dual-Engine Architecture (100% NativeAOT Safe): Features a dedicated
ExpressionInterpreterthat evaluates expression ASTs in memory in just 44.6 nanoseconds without emitting dynamic IL, while preserving an opt-in structural JIT cache (ExpressionCompilationCache) for standard runtimes. - Strict DDD Layer Separation: Domain specifications (
Specification<T>) are strictly pure predicate expressions. Query concerns (sorting, keyset pagination, projection, tracking hints) are isolated in the Application layer via immutable value descriptors (QuerySpec<T>). - Direct AST-to-SQL Translation: Translates expression trees into parameterized SQL AST models (
QueryModel) and renders them across 6 production dialects (PostgreSQL, SQL Server, MySQL, MariaDB, SQLite, Oracle) for zero-overhead Dapper execution. - Parameter Rebinding without
Expression.Invoke: The internalParameterReplacerandExpressionComposerrewrite expression parameters in a single pass, guaranteeing 100% compatibility with LINQ providers and SQL translators. - Compile-Time Architecture Governance: 11 custom Roslyn Analyzers (
SPEC001โSPEC011) flag mutable specifications, missing pagination, un-translatable methods, and legacy Ardalis patterns during compilation.
โก Key Features
- ๐ NativeAOT & Trimming First: Fully verified under .NET 8, 9, and 10 NativeAOT compilers with explicit BCL linker attributes and zero dynamic IL emission on hot paths.
- ๐๏ธ Multi-Dialect SQL Generation: Native parameterized AST rendering for PostgreSQL, SQL Server (
MsSql), MySQL, MariaDB, SQLite, and Oracle Database without ORM dependencies. - ๐งฑ Strict DDD Clean Architecture: Pure domain rules in
Specification<T>, immutable query descriptors inQuerySpec<T>, and repository adapters in Infrastructure. - โก Zero Allocations on Hot Paths: Bounded LRU query plan caching (
QueryPlanCache), structural expression hashing (ExpressionHasher), andReadOnlySpan<T>bulk composition (AndAll/OrAny). - ๐ Keyset & Offset Pagination: First-class support for both high-throughput deterministic keyset seek (
SeekAfter/SeekBefore) and classic offset pagination (Page/Skip/Take). - ๐ก๏ธ Compile-Time Roslyn Governance: 11 analyzers (
SPEC001โSPEC011) with automated CodeFix providers to prevent architectural drift and maintain strict domain purity. - ๐ Enterprise Observability: Integrated OpenTelemetry
ActivitySourceandMeterinstruments tracking specification evaluations, compositions, and SQL translations.
๐ฆ Ecosystem
The EricksonLopez.Specification ecosystem is organized into modular, fine-grained packages following strict Clean Architecture boundaries:
| Package | Version | Description |
|---|---|---|
EricksonLopez.Specification.Abstractions |
Core contracts: ISpecification<T>, QuerySpec<T>, IReadRepository<T>, OrderClause<T> |
|
EricksonLopez.Specification |
Core engine: Specification<T>, Spec factory, ExpressionComposer, ExpressionInterpreter (AOT), ExpressionSimplifier |
|
EricksonLopez.Specification.Linq |
LINQ provider adapter for IQueryable<T>.Apply(querySpec), Any(spec), and Count(spec) |
|
EricksonLopez.Specification.Sql |
AST-based Expression-to-SQL translator (QuerySpecTranslator<T>), QueryModel, ISqlDialect, bounded QueryPlanCache |
|
EricksonLopez.Specification.PostgreSql |
PostgreSQL dialect (PostgreSqlDialect: double quotes, $n parameters, LIMIT/OFFSET, collection predicates) |
|
EricksonLopez.Specification.MsSql |
Microsoft SQL Server dialect (MsSqlDialect: bracket quoting, @pn parameters, TOP N, OFFSET...FETCH) |
|
EricksonLopez.Specification.MySql |
MySQL dialect (MySqlDialect: backtick quoting, @pn parameters, LIMIT/OFFSET, expanded IN) |
|
EricksonLopez.Specification.MariaDb |
MariaDB dialect (MariaDbDialect: backtick quoting, @pn parameters, LIMIT/OFFSET, expanded IN) |
|
EricksonLopez.Specification.Sqlite |
SQLite dialect (SqliteDialect: double quote quoting, @pn parameters, LIMIT/OFFSET) |
|
EricksonLopez.Specification.Oracle |
Oracle Database dialect (OracleDialect: double quotes, :pn positional parameters, OFFSET...FETCH) |
|
EricksonLopez.Specification.Dapper |
High-performance Dapper extension methods over IDbConnection (QueryAsync, CountAsync, AnyAsync) |
|
EricksonLopez.Specification.EntityFrameworkCore |
Entity Framework Core IReadRepository<T> implementation, evaluators, and DI extensions |
|
EricksonLopez.Specification.MongoDB |
MongoDB C# Driver integration (MongoFilterCompiler, MongoSortCompiler, MongoSpecificationExtensions) |
|
EricksonLopez.Specification.DapperExtensions |
Execution adapter over internal EricksonLopez.DapperExtensions Unit-of-Work & session tracking |
|
EricksonLopez.Specification.Result |
Functional Result<T> query extensions over IReadRepository<T> using EricksonLopez.Result |
|
EricksonLopez.Specification.Analyzers |
Roslyn diagnostic analyzers SPEC001โSPEC011 & automated CodeFix providers |
|
EricksonLopez.Specification.Generators |
Roslyn source generator for zero-reflection column resolvers ([SpecColumnResolver]) and ordering helpers |
๐ Documentation
๐ Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-specification/tree/main/docs
๐ Step-by-Step Interactive Showcase (Levels 00 to 10)
Explore progressive runnable showcase levels located in the test and sample harness:
| Level | Topic | Description |
|---|---|---|
| Level 00 | Architecture & Conceptual Foundations | Pure DDD specification principles, expression tree encapsulation, and boundary invariants |
| Level 01 | Quick Start & Domain Primitives | Sealed specifications, Spec.For<T>, Spec.True<T>, and in-memory AOT evaluation |
| Level 02 | Configuration & SQL Dialects | Configuring PostgreSQL, SQL Server, MySQL, SQLite, and Oracle AST translators |
| Level 03 | Real-World Enterprise Use Cases | CQRS queries, compound domain rules, multi-condition validation, and business pipelines |
| Level 04 | Advanced ORM & Database Integrations | EF Core IQueryable.Apply, Dapper parameterized execution, and keyset seek pagination |
| Level 05 | Processing & AST Translation Pipeline | Expression AST visitor rewriting, parameter replacement, and boolean constant folding |
| Level 06 | Error Handling & Invariant Enforcement | Null safety, un-translatable expression handling, and Roslyn diagnostic enforcement |
| Level 07 | Scalability & Bounded LRU Caching | High-throughput query plan caching (QueryPlanCache) and structural expression hashing |
| Level 08 | Customization & Custom Column Resolvers | Custom IColumnNameResolver strategies and source-generated [SpecColumnResolver] |
| Level 09 | Ecosystem Extensions (Result & MongoDB) | Railway-oriented Result<T> query integration and MongoDB Filter/Sort compilation |
| Level 10 | Enterprise Architecture & Domain Isolation | Strict Clean Architecture layer isolation, dependency rule enforcement, and microservice patterns |
๐ Technical Reference & Architecture Guides
- System Architecture Overview โ Comprehensive architectural blueprint, memory layouts, and clean layering boundaries.
- Architectural Decision Records (ADRs) โ 28 official ADRs documenting design rationale, structural tradeoffs, and rejected proposals.
- Features & Technical Matrix โ Detailed feature classification, tier boundaries, and verified competitor comparisons.
- NativeAOT & Trimming Guide โ Linker attributes, AST interpreter node support, and zero-dynamic-code deployment rules.
- Verified Performance Benchmarks โ BenchmarkDotNet suite results across expression composition, in-memory validation, and SQL translation.
- Enterprise Cookbook & Recipes โ 27 production-ready copy-paste recipes for DDD, EF Core, Dapper, NativeAOT, and OpenTelemetry.
- Best Practices & Anti-Patterns โ Architectural rules, coding guidelines, and analyzer diagnostic compliance.
- Competitive Audit & Matrix โ In-depth technical comparison against Ardalis.Specification, LinqKit, and native EF Core.
- Migration from Ardalis.Specification โ Step-by-step guide and Roslyn automated code fixes for legacy migrations.
- Final Quality Audit โ Comprehensive technical audit, Stryker.NET mutation score verification, and quality gates.
๐ฅ Installation
Install the required packages using the .NET CLI or Visual Studio Package Manager:
1. Core Packages (Domain & Application Layers)
# Core contracts and immutable query descriptors (zero dependencies)
dotnet add package EricksonLopez.Specification.Abstractions
# Core specification engine, composite combinators, and AOT interpreter
dotnet add package EricksonLopez.Specification
2. Database Dialects & Providers (Infrastructure Layer โ Dapper / SQL)
# AST-to-SQL translation engine and Dapper execution extensions
dotnet add package EricksonLopez.Specification.Sql
dotnet add package EricksonLopez.Specification.Dapper
# Install your target database dialect:
dotnet add package EricksonLopez.Specification.PostgreSql # PostgreSQL ($1, $2, LIMIT/OFFSET)
dotnet add package EricksonLopez.Specification.MsSql # Microsoft SQL Server (TOP, OFFSET FETCH)
dotnet add package EricksonLopez.Specification.MySql # MySQL (`col`, @p1, LIMIT/OFFSET)
dotnet add package EricksonLopez.Specification.MariaDb # MariaDB (`col`, @p1, LIMIT/OFFSET)
dotnet add package EricksonLopez.Specification.Sqlite # SQLite ("col", @p1, LIMIT/OFFSET)
dotnet add package EricksonLopez.Specification.Oracle # Oracle Database (:p1, OFFSET FETCH)
3. ORM & Document Database Adapters (Infrastructure Layer)
# Entity Framework Core & generic LINQ provider adapter
dotnet add package EricksonLopez.Specification.Linq
dotnet add package EricksonLopez.Specification.EntityFrameworkCore
# MongoDB C# Driver FilterDefinition & SortDefinition compiler
dotnet add package EricksonLopez.Specification.MongoDB
4. Compile-Time Governance & Source Generators (Development Only)
# Roslyn analyzers (SPEC001โSPEC011) and automated CodeFix providers
dotnet add package EricksonLopez.Specification.Analyzers
# Incremental Source Generator for zero-reflection column resolvers
dotnet add package EricksonLopez.Specification.Generators
๐ Quick Start
1. Define a Pure Domain Specification
Domain specifications represent pure business rules. They are immutable, stateless, sealed, and have zero dependencies on ORMs or database drivers.
using System.Linq.Expressions;
using EricksonLopez.Specification;
namespace MyProject.Domain.Specifications;
// Domain layer โ pure, stateless, sealed (enforced by SPEC001 analyzer)
public sealed class ActivePremiumCustomerSpec : Specification<Customer>
{
private readonly decimal _minimumPurchases;
public ActivePremiumCustomerSpec(decimal minimumPurchases = 1_000m)
=> _minimumPurchases = minimumPurchases;
protected override Expression<Func<Customer, bool>> BuildExpression()
=> c => c.IsActive && c.TotalPurchases >= _minimumPurchases && c.DeletedAt == null;
}
2. In-Memory Evaluation & Composition (AOT-Safe)
Evaluate candidates directly in memory without emitting dynamic IL code, or compose multiple business rules using logical combinators:
using EricksonLopez.Specification;
var activeSpec = new ActivePremiumCustomerSpec(minimumPurchases: 500m);
var candidate = new Customer { IsActive = true, TotalPurchases = 750m, DeletedAt = null };
// 100% NativeAOT-safe in-memory evaluation (evaluated via ExpressionInterpreter in ~44.6 ns)
bool isEligible = activeSpec.IsSatisfiedBy(candidate);
// Fluent predicate composition without Expression.Invoke (inlinable by SQL translators)
var verifiedEmailSpec = Spec.For<Customer>(c => c.IsEmailVerified);
var highRiskSpec = Spec.For<Customer>(c => c.RiskScore > 80);
var qualifiedPromoSpec = activeSpec
.And(verifiedEmailSpec)
.And(highRiskSpec.Not());
// Bulk composition using static combinators
Specification<Customer> allRules = Spec.All(
new ActivePremiumCustomerSpec(),
Spec.For<Customer>(c => c.CountryCode == "US"),
Spec.For<Customer>(c => c.CreditLimit > 0m)
);
3. Build an Immutable Query Descriptor (Application Layer)
Use QuerySpec<T> in Application layer handlers to encapsulate filtering, sorting, pagination, and projection as an immutable value record:
using EricksonLopez.Specification;
// Application layer โ immutable query descriptor
var querySpec = QuerySpec<Customer>.Empty
.Where(new ActivePremiumCustomerSpec(1_000m)) // Accepts IExpressionSpecification<T>
.Where(c => c.CountryCode == "US") // Or inline lambda criteria
.OrderByDescending(c => c.TotalPurchases)
.ThenBy(c => c.Name)
.Page(page: 1, pageSize: 25)
.TagWith("Handler:GetTopCustomersQuery");
4. Execute via Entity Framework Core (LINQ Adapter)
Apply query specifications directly to any EF Core IQueryable<T> without leaking ORM concerns into your domain layer:
using Microsoft.EntityFrameworkCore;
using EricksonLopez.Specification.Linq;
// Seamlessly applies WHERE predicates, ORDER BY, SKIP, TAKE, and query tags
List<Customer> topCustomers = await dbContext.Customers
.Apply(querySpec)
.ToListAsync(cancellationToken);
5. Execute via Dapper (Direct Parameterized SQL Generation)
Translate domain specifications into high-performance, injection-safe parameterized SQL without requiring an ORM:
using EricksonLopez.Specification.Sql;
using EricksonLopez.Specification.PostgreSql;
using EricksonLopez.Specification.Dapper;
// 1. Initialize translator with table name and column naming strategy
var translator = new QuerySpecTranslator<Customer>("customers", new SnakeCaseColumnNameResolver());
// 2. Select target dialect (PostgreSQL, SQL Server, MySQL, SQLite, Oracle)
var dialect = PostgreSqlDialect.Default;
// 3. Execute directly over any standard IDbConnection via Dapper extension
IEnumerable<Customer> results = await dbConnection.QueryAsync(
querySpec,
translator,
dialect,
cancellationToken: cancellationToken
);
// Generated SQL:
// SELECT * FROM "customers"
// WHERE "is_active" = $1 AND "total_purchases" >= $2 AND "deleted_at" IS NULL AND "country_code" = $3
// ORDER BY "total_purchases" DESC, "name" ASC
// LIMIT $4 OFFSET $5
๐ก Core Use Cases
Use Case 1: Clean Architecture / CQRS Query Handler with Repository Pattern
Encapsulate data retrieval in CQRS query handlers using IReadRepository<T> without coupling application logic to specific database technologies:
using EricksonLopez.Specification;
public sealed record GetActiveVipCustomersQuery(decimal MinPurchases, int Page, int PageSize)
: IRequest<IReadOnlyList<CustomerDto>>;
public sealed class GetActiveVipCustomersQueryHandler
: IRequestHandler<GetActiveVipCustomersQuery, IReadOnlyList<CustomerDto>>
{
private readonly IReadRepository<Customer> _repository;
public GetActiveVipCustomersQueryHandler(IReadRepository<Customer> repository)
=> _repository = repository;
public async Task<IReadOnlyList<CustomerDto>> Handle(
GetActiveVipCustomersQuery request,
CancellationToken cancellationToken)
{
var spec = QuerySpec<Customer, CustomerDto>.Empty
.Where(new ActivePremiumCustomerSpec(request.MinPurchases))
.OrderByDescending(c => c.TotalPurchases)
.Page(request.Page, request.PageSize)
.Select(c => new CustomerDto(c.Id, c.Name, c.TotalPurchases));
return await _repository.ListAsync(spec, cancellationToken);
}
}
Use Case 2: Multi-Step Domain Rule Composition with Short-Circuiting
Dynamically assemble complex business rules across domain services with automatic boolean constant folding:
public sealed class LoanEligibilityService
{
public bool EvaluateEligibility(Customer customer, LoanApplication application)
{
Specification<Customer> rules = new MinimumAgeSpec(minAge: 21)
.And(new VerifiedIdentitySpec())
.And(new DebtToIncomeRatioSpec(maxRatio: 0.43m));
if (application.RequiresCollateral)
{
rules = rules.And(new VerifiedCollateralSpec(application.RequestedAmount));
}
// Evaluates in-memory with automatic short-circuit semantics
return rules.IsSatisfiedBy(customer);
}
}
Use Case 3: Keyset / Cursor Pagination on High-Volume Datasets
Prevent slow SQL OFFSET table scans on massive datasets using deterministic keyset seek pagination:
// Forward pagination (Next Page after specific cursor ID)
var nextBatchQuery = QuerySpec<Order>.Empty
.Where(o => o.Status == OrderStatus.Completed)
.OrderByDescending(o => o.Id)
.SeekAfter(o => o.Id, cursorValue: 1_084_250L, take: 50);
// Generates: WHERE status = @p1 AND id < @cursor ORDER BY id DESC LIMIT 50
var orders = await _readRepository.ListAsync(nextBatchQuery, cancellationToken);
Use Case 4: NativeAOT Microservices with Interpreted Validation
Deploy lightweight containerized microservices and serverless functions under NativeAOT with zero reflection warnings:
[DapperAot]
public partial class MicroserviceDapperContext : IDapperContext;
// 100% NativeAOT: In-memory evaluation uses interpreted AST walk
var fraudCheck = new NonFlaggedAccountSpec().And(new IpReputationSpec("US"));
if (!fraudCheck.IsSatisfiedBy(account))
{
return Results.Forbid();
}
// Database query rendered via compile-time source generated column resolvers
var results = await connection.QueryAsync(querySpec, aotTranslator, PostgreSqlDialect.Default);
Use Case 5: Multi-Dialect SQL Generation for Dapper & Raw ADO.NET
Execute identical business specifications across heterogeneous database engines:
var spec = new ActivePremiumCustomerSpec().ToQuerySpec().Page(1, 10);
var translator = new QuerySpecTranslator<Customer>("Customers");
// PostgreSQL: SELECT * FROM "Customers" WHERE ... LIMIT 10 OFFSET 0
var pgQuery = PostgreSqlDialect.Default.Render(translator.Translate(spec));
// SQL Server: SELECT * FROM [Customers] WHERE ... OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY
var msSqlQuery = MsSqlDialect.Default.Render(translator.Translate(spec));
// MySQL: SELECT * FROM `Customers` WHERE ... LIMIT 10 OFFSET 0
var mySqlQuery = MySqlDialect.Default.Render(translator.Translate(spec));
// Oracle: SELECT * FROM "Customers" WHERE ... OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY
var oracleQuery = OracleDialect.Default.Render(translator.Translate(spec));
Use Case 6: Functional Result Queries with EricksonLopez.Result
Integrate Railway-Oriented Programming for resilient, exception-free repository queries:
using EricksonLopez.Result;
using EricksonLopez.Specification.Result;
// Returns Result<Customer> instead of throwing exceptions on missing entities
Result<Customer> customerResult = await repository.FirstOrDefaultResultAsync(
QuerySpec<Customer>.Empty.Where(c => c.Email == requestEmail),
cancellationToken
);
return customerResult.Match(
onSuccess: customer => Results.Ok(customer),
onFailure: error => Results.NotFound(error.Message)
);
๐ Configuration & Integrations
Entity Framework Core & Dependency Injection
Register the EF Core specification evaluator and generic read repositories in your IServiceCollection:
using Microsoft.Extensions.DependencyInjection;
using EricksonLopez.Specification.EntityFrameworkCore;
public static void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString("PostgresDb")));
// Registers IReadRepository<T> and ISpecificationEvaluator backed by EF Core
services.AddSpecificationEntityFramework<AppDbContext>();
// Or register explicit typed read repository:
services.AddEfReadRepository<AppDbContext, Customer>();
}
Dapper & Dialect Configuration
Optimize SQL generation with compile-time source-generated column resolvers:
using EricksonLopez.Specification.Generators;
using EricksonLopez.Specification.PostgreSql;
using EricksonLopez.Specification.Sql;
// 1. Declare source-generated column resolver (eliminates runtime reflection)
[SpecColumnResolver(typeof(Customer), Convention = NamingConvention.SnakeCase)]
public sealed partial class CustomerColumnResolver;
// 2. Register translator in DI as singleton
services.AddSingleton(new QuerySpecTranslator<Customer>("customers", new CustomerColumnResolver()));
services.AddSingleton<ISqlDialect>(PostgreSqlDialect.Default);
MongoDB Driver Integration
Compile specification expressions directly into native MongoDB FilterDefinition<T> and SortDefinition<T>:
using EricksonLopez.Specification.MongoDB;
using MongoDB.Driver;
var querySpec = QuerySpec<CustomerDoc>.Empty
.Where(c => c.IsActive && c.Score >= 100)
.OrderByDescending(c => c.CreatedAt)
.Page(1, 20);
// Compile to MongoDB native filter and sort definitions
FilterDefinition<CustomerDoc> filter = MongoFilterCompiler.Compile(querySpec);
SortDefinition<CustomerDoc> sort = MongoSortCompiler.Compile(querySpec);
var results = await mongoCollection
.Find(filter)
.Sort(sort)
.Skip(querySpec.Pagination?.Skip)
.Limit(querySpec.Pagination?.Take)
.ToListAsync(cancellationToken);
OpenTelemetry Metrics & Diagnostics
Monitor specification evaluations, composition overhead, and SQL translation counts in production:
using OpenTelemetry.Metrics;
var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("EricksonLopez.Specification")
.AddPrometheusExporter()
.Build();
// Automatically recorded metrics:
// - specification.evaluations_total (Counter)
// - specification.compositions_total (Counter)
// - specification.sql_translations_total (Counter)
Compile-Time Roslyn Analyzers (SPEC001โSPEC011)
The library includes 11 automated analyzers to enforce architectural purity and prevent misuse during compilation:
| Diagnostic ID | Severity | Category | Description | CodeFix Available |
|---|---|---|---|---|
SPEC001 |
Warning | Architecture | Specification classes must be declared sealed or abstract. |
โ Yes |
SPEC002 |
Warning | Immutability | Specifications must not contain mutable state, fields, or properties. | โ No |
SPEC003 |
Error | Correctness | Expression.Invoke is prohibited in specification expression trees. |
โ No |
SPEC004 |
Info | Performance | Unbounded query detected; recommend adding .Take() or .Page(). |
โ No |
SPEC005 |
Info | Correctness | Ordering clause applied without pagination limits. | โ No |
SPEC006 |
Info | Layering | Domain specification declared outside Domain layer boundary. | โ No |
SPEC007 |
Warning | SQL Translation | Non-translatable method invocation inside BuildExpression. |
โ No |
SPEC008 |
Error | Purity | Prohibits infrastructure dependencies (DbContext, IServiceProvider) in constructors. |
โ No |
SPEC009 |
Error | Correctness | Disallows async lambdas inside BuildExpression. |
โ No |
SPEC010 |
Error | Correctness | Disallows calling IsSatisfiedBy inside BuildExpression. |
โ No |
SPEC011 |
Warning | Migration | Flags inheritance from legacy Ardalis.Specification base class. |
โ Yes |
๐งช Testing & Quality
In-Memory Unit Testing
Test pure domain specifications in unit test projects without databases or mocks:
using Xunit;
using EricksonLopez.Specification;
public sealed class ActivePremiumCustomerSpecTests
{
[Fact]
public void IsSatisfiedBy_WhenCustomerIsActiveAndMeetsPurchases_ReturnsTrue()
{
// Arrange
var spec = new ActivePremiumCustomerSpec(minimumPurchases: 1_000m);
var customer = new Customer { IsActive = true, TotalPurchases = 1_500m, DeletedAt = null };
// Act
bool result = spec.IsSatisfiedBy(customer);
// Assert
Assert.True(result);
}
[Fact]
public void IsSatisfiedBy_WhenCustomerIsDeleted_ReturnsFalse()
{
// Arrange
var spec = new ActivePremiumCustomerSpec(minimumPurchases: 1_000m);
var customer = new Customer { IsActive = true, TotalPurchases = 2_000m, DeletedAt = DateTime.UtcNow };
// Act
bool result = spec.IsSatisfiedBy(customer);
// Assert
Assert.False(result);
}
}
SQL Translation Snapshot Verification
Verify AST translation output deterministically:
[Fact]
public void Translate_WithFilterAndPaging_GeneratesExpectedPostgreSql()
{
var spec = QuerySpec<Customer>.Empty
.Where(c => c.IsActive)
.OrderByDescending(c => c.CreatedAt)
.Page(page: 2, pageSize: 10);
var translator = new QuerySpecTranslator<Customer>("customers", new SnakeCaseColumnNameResolver());
var model = translator.Translate(spec);
var sqlQuery = PostgreSqlDialect.Default.Render(model);
Assert.Equal(
"SELECT * FROM \"customers\" WHERE \"is_active\" = $1 ORDER BY \"created_at\" DESC LIMIT $2 OFFSET $3",
sqlQuery.Sql
);
Assert.Equal(true, sqlQuery.Parameters["$1"]);
Assert.Equal(10, sqlQuery.Parameters["$2"]);
Assert.Equal(10, sqlQuery.Parameters["$3"]);
}
Mutation Testing & Quality Score
Every release undergoes exhaustive mutation testing via Stryker.NET to ensure test assertions catch all logic regressions:
- Mutation Score: โฅ 99%
- Test Invariants: 100% coverage of boolean folding, parameter rewriting, and dialect rendering nodes.
- Linker Verification: Zero trimming warnings under .NET SDK 10.0 NativeAOT compiler.
โก Performance Benchmarks
Environment: .NET 10.0.10 (10.0.1026.32716), X64 RyuJIT AVX-512, Windows 11 Pro, BenchmarkDotNet v0.14.0
Primary Operations & Composition
Compairing combining predicates (c => c.IsActive and c => !c.IsDeleted) via ExpressionComposer.And versus manual dynamic lambda construction:
| Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|
Manual: x => left && right |
184.71 ns | 1.00 | 0.0110 | 560 B | 1.00 |
EricksonLopez: ExpressionComposer.And |
93.51 ns | 0.51 | 0.0088 | 448 B | 0.80 |
EricksonLopez: 5-way AND Composition |
343.08 ns | 1.86 | 0.0343 | 1,736 B | 3.10 |
Key Takeaway:
ExpressionComposer.Andoutperforms manual lambda creation by 49% in execution time and 20% in memory allocation via single-pass parameter rebinding.
In-Memory Evaluation Benchmark (AOT vs JIT)
Evaluates a composite specification (ActiveCustomerSpec.And(NotDeletedSpec)) against a candidate instance:
| Method | Mean | Median | Gen0 | Allocated | Execution Mode |
|---|---|---|---|---|---|
| Manual Direct Delegate (JIT Inlined) | 0.0017 ns | 0.0004 ns | - | - | Direct compiled C# delegate |
EricksonLopez: IsSatisfiedBy (Interpreted) |
44.64 ns | 44.63 ns | 0.0019 | 96 B | 100% NativeAOT Safe (Zero Dynamic IL) |
EricksonLopez: IsSatisfiedBy (Compiled Cache) |
64.60 ns | 64.49 ns | 0.0010 | 48 B | JIT Cached Structural Delegate |
Key Takeaway: In-memory interpreted evaluation executes in just 44.6 nanoseconds, enabling sub-microsecond validation on NativeAOT without dynamic code generation.
SQL AST Translation Benchmark
Measures translating a QuerySpec<T> into a parameterized SQL string and parameter dictionary for PostgreSQL:
| Method | Mean | Gen0 | Allocated | Description |
|---|---|---|---|---|
| EricksonLopez: Simple Spec โ SQL | 108.63 ns | 0.0225 | 1.11 KB | Single filter (WHERE is_active = $1) |
| EricksonLopez: Complex Spec โ SQL | 412.62 ns | 0.0634 | 3.13 KB | 3 filters + ORDER BY + LIMIT/OFFSET |
LINQ Provider Overhead (QuerySpec.Apply)
Measures applying a QuerySpec<T> with filtering, sorting, and paging against an IQueryable<T> data source of 1,000 entities:
| Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|
| Manual LINQ Query | 688.7 ฮผs | 1.00 | 0.9766 | 67.96 KB | 1.00 |
EricksonLopez: QuerySpec.Apply |
702.2 ฮผs | 1.02 | 0.9766 | 67.12 KB | 0.99 |
Span-Based Bulk Predicate Composition (AndAll)
Bulk composition of 5 predicates using ReadOnlySpan<T> versus chained .And() invocations:
| Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|
Chained .And() ร 4 |
520.0 ns | 1.00 | 0.0391 | 1.94 KB | 1.00 |
AndAll(ReadOnlySpan) ร 5 |
477.6 ns | 0.92 | 0.0391 | 1.94 KB | 1.00 |
๐ Compatibility & Technical Matrix
Runtime & Target Framework Matrix
| Package | .NET 8.0 LTS | .NET 9.0 STS | .NET 10.0 | NativeAOT | Trimmable | Linker Notes |
|---|---|---|---|---|---|---|
EricksonLopez.Specification.Abstractions |
โ Full | โ Full | โ Full | โ Full | โ Full | BCL-only pure contracts |
EricksonLopez.Specification |
โ Full | โ Full | โ Full | โ Full | โ Full | Core AOT safe; JIT cache marked [RequiresDynamicCode] |
EricksonLopez.Specification.Linq |
โ Full | โ Full | โ Full | โ Full | โ Full | Passes expressions to LINQ provider |
EricksonLopez.Specification.Sql |
โ Full | โ Full | โ Full | โ ๏ธ Annotated | โ Full | Reflection closures annotated [RequiresUnreferencedCode] |
EricksonLopez.Specification.PostgreSql |
โ Full | โ Full | โ Full | โ Full | โ Full | Pure AST string renderer |
EricksonLopez.Specification.MsSql |
โ Full | โ Full | โ Full | โ Full | โ Full | Pure AST string renderer |
EricksonLopez.Specification.MySql |
โ Full | โ Full | โ Full | โ Full | โ Full | Pure AST string renderer |
EricksonLopez.Specification.MariaDb |
โ Full | โ Full | โ Full | โ Full | โ Full | Pure AST string renderer |
EricksonLopez.Specification.Sqlite |
โ Full | โ Full | โ Full | โ Full | โ Full | Pure AST string renderer |
EricksonLopez.Specification.Oracle |
โ Full | โ Full | โ Full | โ Full | โ Full | Pure AST string renderer |
EricksonLopez.Specification.Dapper |
โ Full | โ Full | โ Full | โ Full | โ Full | Fully compatible with Dapper.AOT |
EricksonLopez.Specification.EntityFrameworkCore |
โ Full | โ Full | โ Full | โ Full | โ Full | Compatible with EF Core compiled models |
EricksonLopez.Specification.MongoDB |
โ Full | โ Full | โ Full | โ Full | โ Full | Compatible with MongoDB Driver v3+ |
EricksonLopez.Specification.Analyzers |
N/A | N/A | N/A | N/A | N/A | Roslyn compile-time only |
SQL Dialects Feature Matrix
| Dialect | Identifier Quote | Parameter Format | Paging Clause | String Matching | Collection Predicates |
|---|---|---|---|---|---|
| PostgreSQL | "column" |
$1, $2, ... |
LIMIT n OFFSET m |
LIKE, ILIKE |
= ANY(@p) |
| SQL Server | [column] |
@p1, @p2, ... |
OFFSET m ROWS FETCH NEXT n ROWS ONLY |
LIKE |
IN (@p1, @p2) |
| MySQL | `column` |
@p1, @p2, ... |
LIMIT n OFFSET m |
LIKE |
IN (@p1, @p2) |
| MariaDB | `column` |
@p1, @p2, ... |
LIMIT n OFFSET m |
LIKE |
IN (@p1, @p2) |
| SQLite | "column" |
@p1, @p2, ... |
LIMIT n OFFSET m |
LIKE |
IN (@p1, @p2) |
| Oracle | "COLUMN" |
:p1, :p2, ... |
OFFSET m ROWS FETCH NEXT n ROWS ONLY |
LIKE |
IN (:p1, :p2) |
๐๏ธ Architecture & Design Principles
System Flow & Layer Boundaries
The following diagram illustrates how domain business rules travel from the Domain layer into application query descriptors and execute through LINQ or native SQL infrastructure:
flowchart TD
%% Architecture Layers
subgraph Domain["Domain Layer"]
Spec["Specification<T>"]
SpecDesc["Encapsulates Pure Predicates as Expressions"]
Spec --- SpecDesc
end
subgraph Application["Application Layer"]
QuerySpec["QuerySpec<T> / QuerySpec<T, TResult>"]
QueryDesc["Adds Sorting, Keyset/Offset Paging, & Projection"]
QuerySpec --- QueryDesc
end
subgraph CoreEngine["Specification Engine"]
Composer["ExpressionComposer (Parameter Replacer)"]
Simplifier["ExpressionSimplifier (Constant Folding)"]
Hasher["ExpressionHasher (Structural Equality)"]
end
subgraph Infrastructure["Infrastructure Layer"]
direction LR
subgraph LinqProvider["LINQ Provider (EF Core)"]
ApplyExt["QuerySpecLinqExtensions.Apply"]
IQueryable["IQueryable<T>"]
end
subgraph SqlProvider["SQL Provider (Dapper)"]
Translator["QuerySpecTranslator<T>"]
QueryModel["QueryModel AST"]
Dialect["ISqlDialect (PostgreSQL, SQL Server, MySQL, SQLite, Oracle)"]
SqlQuery["SqlQuery (Parameterized SQL + Parameters)"]
end
end
%% Relationships
Spec -->|.And() / .Or() / .Not()| Composer
Composer --> Simplifier
Simplifier --> QuerySpec
Spec -->|Direct Where| QuerySpec
QuerySpec -->|LINQ Path| ApplyExt
ApplyExt --> IQueryable
IQueryable -->|EF Core Execution| Database[(Database)]
QuerySpec -->|Native SQL Path| Translator
Translator -->|Translates to AST| QueryModel
QueryModel -->|Renders Dialect| Dialect
Dialect --> SqlQuery
SqlQuery -->|Executes via Dapper| Database
Expression Lifecycle State Machine
The state machine below depicts the transformation of specification expressions through simplification, caching, and multi-path provider execution:
stateDiagram-v2
[*] --> ExpressionTree: Instantiate Specification<T>
state ExpressionTree {
[*] --> Unsimplified
Unsimplified --> Simplified: ExpressionSimplifier.Simplify()
Simplified --> [*]
}
ExpressionTree --> InterpretedAOT: spec.IsSatisfiedBy(candidate)
InterpretedAOT --> Evaluated: ExpressionInterpreter AST Walk (Zero IL)
ExpressionTree --> JITCompiled: spec.ToCompiledPredicate()
JITCompiled --> Evaluated: ExpressionCompilationCache (JIT Only)
ExpressionTree --> IQueryable: QuerySpecLinqExtensions.Apply()
IQueryable --> EFCoreSQL: EF Core Query Translation
ExpressionTree --> SqlAST: QuerySpecTranslator.Translate()
SqlAST --> ParameterizedSQL: ISqlDialect.Render()
EFCoreSQL --> Executed: Provider ExecuteAsync()
ParameterizedSQL --> Executed: Dapper connection.QueryAsync()
Evaluated --> [*]
Executed --> [*]
๐ก๏ธ Best Practices & Anti-Patterns
| Scenario | โ Avoid | โ Recommended |
|---|---|---|
| Class Declaration | Declaring unsealed specification classes | Sealing all concrete specifications (SPEC001) |
| Specification State | Adding mutable fields/properties to specifications | Using readonly constructor parameters captured in closures (SPEC002) |
| Expression Composition | Using Expression.Invoke to combine lambdas |
Using .And(), .Or(), and ExpressionComposer (SPEC003) |
| Query Limits | Executing unbounded queries without pagination | Always applying .Page() or .Take() on database queries (SPEC004) |
| Ordering & Paging | Paginating without a deterministic ordering | Specifying .OrderBy() before applying pagination clauses (SPEC005) |
| Clean Architecture | Referencing DbContext or IDbConnection in Domain specs |
Keeping specifications pure and injecting IReadRepository<T> in Application (SPEC008) |
| Async Operations | Using async / await inside BuildExpression |
Keeping expression trees pure and executing async in repositories (SPEC009) |
| In-Memory Validation in AOT | Calling spec.ToCompiledPredicate() under NativeAOT |
Using spec.IsSatisfiedBy() (evaluates via ExpressionInterpreter) |
| High-Volume Pagination | Using large OFFSET values on big tables |
Using keyset pagination via .SeekAfter() / .SeekBefore() |
| SQL Column Mapping | Relying on runtime reflection in AOT paths | Using source-generated column resolvers ([SpecColumnResolver]) |
โ ๏ธ Troubleshooting & Common Pitfalls
Domain Invariants & NativeAOT Safety Rules:
- Do not use
ToCompiledPredicate()in NativeAOT: In NativeAOT runtimes, dynamic IL generation viaExpression.Compile()throws at runtime. Always usespec.IsSatisfiedBy(candidate)which executes safely throughExpressionInterpreter. - Do not inject infrastructure services into specifications: Domain specifications represent business rules, not services. Injecting
DbContext,IHttpClientFactory, orIServiceProvidertriggers compilation errorSPEC008. - Avoid non-translatable C# methods in expressions: Calling custom C# methods inside
BuildExpressionthat cannot be translated to SQL will triggerSPEC007and cause LINQ/Dapper provider execution failures. - Do not omit ordering when paginating: SQL engines do not guarantee deterministic result ordering without an explicit
ORDER BY. Always add.OrderBy()when calling.Page()or.Take().
๐ Part of the EricksonLopez Ecosystem
EricksonLopez.Specification is part of the standardized, high-performance .NET enterprise ecosystem:
- ๐งฑ EricksonLopez.SharedKernel โ Foundational domain primitives, value objects, entity identifiers, and domain event dispatching.
- โก EricksonLopez.Result โ High-performance, zero-allocation struct-based Result Pattern & Railway-Oriented Programming.
- ๐ EricksonLopez.Specification โ Composable, AOT-first Specification Pattern & multi-dialect SQL AST translation.
- ๐ฌ EricksonLopez.Mediator โ Zero-allocation, source-generated mediator and CQRS pipeline engine.
- ๐ EricksonLopez.Concurrency โ Resilient asynchronous concurrency primitives, rate limiting, and execution gates.
- ๐ข EricksonLopez.MultiTenancy โ High-isolation multitenancy infrastructure with PostgreSQL Row-Level Security (RLS).
๐ค Contributing
Contributions, bug reports, and pull requests are welcome!
Local Development Setup
Prerequisites:
- .NET SDK 10.0 (or .NET SDK 8.0/9.0)
- Docker (optional, for running integration test database containers)
Clone & Build:
git clone https://github.com/ericksonlopezf/dotnet-specification.git cd dotnet-specification dotnet restore EricksonLopez.Specifications.slnx dotnet build EricksonLopez.Specifications.slnx -c ReleaseRun Unit Tests:
dotnet test EricksonLopez.Specifications.slnx -c Release --no-buildRun Mutation Testing (Stryker.NET):
dotnet tool restore dotnet stryker --config-file-path stryker-config.json
Please read our Contributing Guide, Code of Conduct, and Security Policy before submitting pull requests.
๐ License
Distributed under the MIT License. Copyright ยฉ 2026 Erickson Lopez.
| 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
- EricksonLopez.Specification.Sql (>= 1.0.0)
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 |
|---|---|---|
| 1.0.0 | 36 | 8/28/2026 |