EricksonLopez.Specification.Analyzers 1.0.0

dotnet add package EricksonLopez.Specification.Analyzers --version 1.0.0
                    
NuGet\Install-Package EricksonLopez.Specification.Analyzers -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EricksonLopez.Specification.Analyzers" Version="1.0.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EricksonLopez.Specification.Analyzers" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="EricksonLopez.Specification.Analyzers">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EricksonLopez.Specification.Analyzers --version 1.0.0
                    
#r "nuget: EricksonLopez.Specification.Analyzers, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EricksonLopez.Specification.Analyzers@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EricksonLopez.Specification.Analyzers&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=EricksonLopez.Specification.Analyzers&version=1.0.0
                    
Install as a Cake Tool

EricksonLopez.Specification

High-performance, composable, NativeAOT-first Specification Pattern and SQL AST translation ecosystem for modern .NET.

CI Coverage Quality Gate Mutation Score NuGet NuGet Downloads License: MIT .NET NativeAOT


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

The Pain Points of Traditional Specification Libraries

  1. 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.
  2. 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.
  3. 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.
  4. Mutable State & Expression.Invoke Provider Failures: Combining expressions using Expression.Invoke breaks 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 ExpressionInterpreter that 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 internal ParameterReplacer and ExpressionComposer rewrite 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 in QuerySpec<T>, and repository adapters in Infrastructure.
  • โšก Zero Allocations on Hot Paths: Bounded LRU query plan caching (QueryPlanCache), structural expression hashing (ExpressionHasher), and ReadOnlySpan<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 ActivitySource and Meter instruments 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 NuGet Core contracts: ISpecification<T>, QuerySpec<T>, IReadRepository<T>, OrderClause<T>
EricksonLopez.Specification NuGet Core engine: Specification<T>, Spec factory, ExpressionComposer, ExpressionInterpreter (AOT), ExpressionSimplifier
EricksonLopez.Specification.Linq NuGet LINQ provider adapter for IQueryable<T>.Apply(querySpec), Any(spec), and Count(spec)
EricksonLopez.Specification.Sql NuGet AST-based Expression-to-SQL translator (QuerySpecTranslator<T>), QueryModel, ISqlDialect, bounded QueryPlanCache
EricksonLopez.Specification.PostgreSql NuGet PostgreSQL dialect (PostgreSqlDialect: double quotes, $n parameters, LIMIT/OFFSET, collection predicates)
EricksonLopez.Specification.MsSql NuGet Microsoft SQL Server dialect (MsSqlDialect: bracket quoting, @pn parameters, TOP N, OFFSET...FETCH)
EricksonLopez.Specification.MySql NuGet MySQL dialect (MySqlDialect: backtick quoting, @pn parameters, LIMIT/OFFSET, expanded IN)
EricksonLopez.Specification.MariaDb NuGet MariaDB dialect (MariaDbDialect: backtick quoting, @pn parameters, LIMIT/OFFSET, expanded IN)
EricksonLopez.Specification.Sqlite NuGet SQLite dialect (SqliteDialect: double quote quoting, @pn parameters, LIMIT/OFFSET)
EricksonLopez.Specification.Oracle NuGet Oracle Database dialect (OracleDialect: double quotes, :pn positional parameters, OFFSET...FETCH)
EricksonLopez.Specification.Dapper NuGet High-performance Dapper extension methods over IDbConnection (QueryAsync, CountAsync, AnyAsync)
EricksonLopez.Specification.EntityFrameworkCore NuGet Entity Framework Core IReadRepository<T> implementation, evaluators, and DI extensions
EricksonLopez.Specification.MongoDB NuGet MongoDB C# Driver integration (MongoFilterCompiler, MongoSortCompiler, MongoSpecificationExtensions)
EricksonLopez.Specification.DapperExtensions NuGet Execution adapter over internal EricksonLopez.DapperExtensions Unit-of-Work & session tracking
EricksonLopez.Specification.Result NuGet Functional Result<T> query extensions over IReadRepository<T> using EricksonLopez.Result
EricksonLopez.Specification.Analyzers NuGet Roslyn diagnostic analyzers SPEC001โ€“SPEC011 & automated CodeFix providers
EricksonLopez.Specification.Generators NuGet 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


๐Ÿ“ฅ 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.And outperforms 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&lt;T&gt;"]
        SpecDesc["Encapsulates Pure Predicates as Expressions"]
        Spec --- SpecDesc
    end

    subgraph Application["Application Layer"]
        QuerySpec["QuerySpec&lt;T&gt; / QuerySpec&lt;T, TResult&gt;"]
        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&lt;T&gt;"]
        end
        subgraph SqlProvider["SQL Provider (Dapper)"]
            Translator["QuerySpecTranslator&lt;T&gt;"]
            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&lt;T&gt;
    
    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:

  1. Do not use ToCompiledPredicate() in NativeAOT: In NativeAOT runtimes, dynamic IL generation via Expression.Compile() throws at runtime. Always use spec.IsSatisfiedBy(candidate) which executes safely through ExpressionInterpreter.
  2. Do not inject infrastructure services into specifications: Domain specifications represent business rules, not services. Injecting DbContext, IHttpClientFactory, or IServiceProvider triggers compilation error SPEC008.
  3. Avoid non-translatable C# methods in expressions: Calling custom C# methods inside BuildExpression that cannot be translated to SQL will trigger SPEC007 and cause LINQ/Dapper provider execution failures.
  4. 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

  1. Prerequisites:

    • .NET SDK 10.0 (or .NET SDK 8.0/9.0)
    • Docker (optional, for running integration test database containers)
  2. 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 Release
    
  3. Run Unit Tests:

    dotnet test EricksonLopez.Specifications.slnx -c Release --no-build
    
  4. Run 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.

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

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 40 8/28/2026