DesignPatterns.Specification.Net.EntityFrameworkCore 26.2.11.1

dotnet add package DesignPatterns.Specification.Net.EntityFrameworkCore --version 26.2.11.1
                    
NuGet\Install-Package DesignPatterns.Specification.Net.EntityFrameworkCore -Version 26.2.11.1
                    
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="DesignPatterns.Specification.Net.EntityFrameworkCore" Version="26.2.11.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DesignPatterns.Specification.Net.EntityFrameworkCore" Version="26.2.11.1" />
                    
Directory.Packages.props
<PackageReference Include="DesignPatterns.Specification.Net.EntityFrameworkCore" />
                    
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 DesignPatterns.Specification.Net.EntityFrameworkCore --version 26.2.11.1
                    
#r "nuget: DesignPatterns.Specification.Net.EntityFrameworkCore, 26.2.11.1"
                    
#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 DesignPatterns.Specification.Net.EntityFrameworkCore@26.2.11.1
                    
#: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=DesignPatterns.Specification.Net.EntityFrameworkCore&version=26.2.11.1
                    
Install as a Cake Addin
#tool nuget:?package=DesignPatterns.Specification.Net.EntityFrameworkCore&version=26.2.11.1
                    
Install as a Cake Tool

DesignPatterns.Specification.Net.EntityFrameworkCore

NuGet License: MIT

First-class Entity Framework Core integration for the DesignPatterns.Specification.Net library. Provides EF Core-aware evaluators, DbSet extensions, repository base classes, and optimized query translation for specifications.


๐Ÿ“ฆ Installation

# Install both core and EF Core packages
dotnet add package DesignPatterns.Specification.Net
dotnet add package DesignPatterns.Specification.Net.EntityFrameworkCore

Requirements:

  • .NET 9.0 or .NET 10.0
  • Entity Framework Core 9.x or 10.x (matched to your target framework)

๐ŸŽฏ What This Package Provides

โœ… EF Core-Specific Evaluators

Translates specification rules into optimized EF Core queries:

  • WhereEvaluator: Applies filter criteria to IQueryable<T>
  • IncludeEvaluator: Handles Include() and ThenInclude() with optional delegate caching
  • OrderEvaluator: Translates ordering expressions (OrderBy, ThenBy)
  • PaginationEvaluator: Applies Skip() and Take() for pagination
  • SearchEvaluator: Converts search patterns to EF.Functions.Like()
  • AsNoTrackingEvaluator: Configures change tracking behavior
  • AsNoTrackingWithIdentityResolutionEvaluator: No tracking with identity resolution
  • AsTrackingEvaluator: Enables change tracking
  • AsSplitQueryEvaluator: Splits queries for better performance with multiple includes
  • IgnoreQueryFiltersEvaluator: Ignores global query filters

โœ… DbSet/IQueryable Extensions

Convenient extension methods in the Microsoft.EntityFrameworkCore namespace:

  • WithSpecification(spec): Apply specification to any IQueryable<T>
  • ToListAsync(spec, ct): Materialize with post-processing
  • ToEnumerableAsync(spec, ct): Lazy enumeration with post-processing
  • Search(criteria): Apply search patterns using EF.Functions.Like()

โœ… Repository Base Classes

Production-ready repository implementations:

  • RepositoryBase<T>: Standard repository with DbContext
  • ContextFactoryRepositoryBase<TEntity, TContext>: Uses IDbContextFactory<T> for per-operation contexts
  • EFRepositoryFactory: Factory pattern for creating repositories

๐Ÿš€ Quick Start

1. Define Your DbContext

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) 
        : base(options) { }

    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();
}

2. Create a Repository

using DesignPatterns.Specification.Net.EntityFrameworkCore;

public class CustomerRepository : RepositoryBase<Customer>
{
    public CustomerRepository(AppDbContext context) : base(context)
    {
        // NoTracking is set by default in the base class
    }
}

3. Use Specifications with Your Repository

public class CustomerService
{
    private readonly CustomerRepository _repository;

    public CustomerService(CustomerRepository repository)
    {
        _repository = repository;
    }

    public async Task<List<Customer>> GetActiveCustomersAsync(CancellationToken ct)
    {
        var spec = new ActiveCustomersSpec();
        return await _repository.ListAsync(spec, ct);
    }

    public async Task<Customer?> GetCustomerByIdAsync(int id, CancellationToken ct)
    {
        var spec = new CustomerByIdSpec(id);
        return await _repository.FirstOrDefaultAsync(spec, ct);
    }

    public async Task<int> CountVipCustomersAsync(CancellationToken ct)
    {
        var spec = new VipCustomersSpec();
        return await _repository.CountAsync(spec, ct);
    }
}

๐Ÿ”ง Advanced Features

๐Ÿš€ Include Delegate Caching

For specifications with complex include chains, enable caching to reduce reflection overhead:

// Option 1: Use the cached singleton
services.AddSingleton<ISpecificationEvaluator>(
    SpecificationEvaluator.Cached
);

// Option 2: Create custom evaluator with caching
var evaluator = new SpecificationEvaluator(cacheEnabled: true);

When to use caching:

  • You have many includes and ThenIncludes
  • Specifications are executed frequently
  • Performance profiling shows Include evaluation as a bottleneck

Performance impact:

  • First call: Compiles and caches the delegate (slight overhead)
  • Subsequent calls: Reuses cached delegate (significant speedup)

Example Specification with Complex Includes

public class OrderWithDetailsSpec : Specification<Order>
{
    public OrderWithDetailsSpec(int orderId)
    {
        Query
            .Where(o => o.Id == orderId)
            .Include(o => o.Customer)
                .ThenInclude(c => c.Address)
            .Include(o => o.OrderItems)
                .ThenInclude(oi => oi.Product)
                    .ThenInclude(p => p.Category)
            .Include(o => o.ShippingAddress)
            .AsNoTracking();

        // With caching enabled, the Include delegates are compiled once
    }
}

๐Ÿ“š DbSet Extensions

WithSpecification

Apply a specification to any IQueryable<T>:

using Microsoft.EntityFrameworkCore;

var query = dbContext.Customers
    .WithSpecification(new ActiveCustomersSpec());

var customers = await query.ToListAsync(cancellationToken);

WithSpecification (Projection)

Apply a specification with projection:

var query = dbContext.Customers
    .WithSpecification(new CustomerNamesSpec()); // Specification<Customer, string>

List<string> names = await query.ToListAsync(cancellationToken);

ToListAsync / ToEnumerableAsync

Materialize results and run post-processing actions:

// Automatically runs PostProcessingAction if defined in spec
var customers = await dbContext.Customers
    .ToListAsync(new ActiveCustomersSpec(), cancellationToken);

// Lazy evaluation (IAsyncEnumerable)
await foreach (var customer in dbContext.Customers
    .ToEnumerableAsync(new ActiveCustomersSpec(), cancellationToken))
{
    Console.WriteLine(customer.Name);
}

๐Ÿ” Search with EF.Functions.Like

The SearchEvaluator automatically translates search patterns to EF.Functions.Like():

public class CustomerSearchSpec : Specification<Customer>
{
    public CustomerSearchSpec(string searchTerm)
    {
        Query
            .Search(c => c.Name, $"%{searchTerm}%")
            .Search(c => c.Email, $"%{searchTerm}%")
            .OrderBy(c => c.Name);
    }
}

Supported Search Patterns

Pattern Description Example
% Matches any sequence of characters "%smith%" โ†’ Contains "smith"
_ Matches any single character "_ob" โ†’ "Bob", "Rob", "Job"
[A-Z] Matches any character in range "[A-C]%" โ†’ Starts with A, B, or C
[^A-Z] Matches any character NOT in range "[^A-C]%" โ†’ Doesn't start with A, B, C

In-Memory vs EF Core

// EF Core: Translates to SQL LIKE
var customers = await dbContext.Customers
    .ToListAsync(new CustomerSearchSpec("john"), ct);
// SQL: WHERE Name LIKE '%john%' OR Email LIKE '%john%'

// In-Memory: Uses custom Like extension
var customers = new CustomerSearchSpec("john").Evaluate(customerList);

๐Ÿ—„๏ธ Repository Patterns

RepositoryBase<T>

Standard repository that uses a single DbContext instance:

public class CustomerRepository : RepositoryBase<Customer>
{
    public CustomerRepository(AppDbContext context) : base(context)
    {
    }

    // Add custom methods if needed
    public async Task<List<Customer>> GetVipCustomersWithOrdersAsync(CancellationToken ct)
    {
        return await ListAsync(new VipCustomersSpec(), ct);
    }
}

Key features:

  • Sets QueryTrackingBehavior.NoTracking by default
  • Implements IRepositoryBase<T> from core library
  • Optimizes Count/Any operations (excludes includes/projections)
  • Supports all specification features

ContextFactoryRepositoryBase<TEntity, TContext>

For long-lived services (e.g., Blazor components, desktop apps):

public class CustomerRepository : ContextFactoryRepositoryBase<Customer, AppDbContext>
{
    public CustomerRepository(IDbContextFactory<AppDbContext> factory) 
        : base(factory)
    {
    }
}

Advantages:

  • Creates and disposes DbContext per operation
  • Safe for long-lived component lifetimes
  • Avoids DbContext threading issues
  • Ideal for UI scenarios

Setup:

// Program.cs or Startup.cs
services.AddDbContextFactory<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

services.AddScoped<CustomerRepository>();

EFRepositoryFactory

Factory pattern for creating repositories:

public interface ICustomerRepository : IReadRepositoryBase<Customer> { }

public class CustomerRepository : RepositoryBase<Customer>, ICustomerRepository
{
    public CustomerRepository(AppDbContext context) : base(context) { }
}

// Register factory
services.AddDbContextFactory<AppDbContext>(/*...*/);
services.AddScoped<IRepositoryFactory<ICustomerRepository>, 
    EFRepositoryFactory<ICustomerRepository, CustomerRepository, AppDbContext>>();

// Usage
public class CustomerService
{
    private readonly IRepositoryFactory<ICustomerRepository> _repoFactory;

    public CustomerService(IRepositoryFactory<ICustomerRepository> repoFactory)
    {
        _repoFactory = repoFactory;
    }

    public async Task<Customer?> GetCustomerAsync(int id, CancellationToken ct)
    {
        var repo = _repoFactory.CreateRepository();
        return await repo.FirstOrDefaultAsync(new CustomerByIdSpec(id), ct);
    }
}

โšก Query Optimization Features

AsNoTracking / AsTracking

Control Entity Framework change tracking:

public class ReadOnlyCustomersSpec : Specification<Customer>
{
    public ReadOnlyCustomersSpec()
    {
        Query
            .Where(c => c.IsActive)
            .AsNoTracking(); // Better performance for read-only queries
    }
}

public class EditableCustomersSpec : Specification<Customer>
{
    public EditableCustomersSpec()
    {
        Query
            .Where(c => c.IsActive)
            .AsTracking(); // Enable change tracking for updates
    }
}

AsNoTrackingWithIdentityResolution

When you need no tracking but want identity resolution:

public class CustomersWithIdentityResolutionSpec : Specification<Customer>
{
    public CustomersWithIdentityResolutionSpec()
    {
        Query
            .Include(c => c.Orders)
            .AsNoTrackingWithIdentityResolution(); // Resolves same entity instances
    }
}

Use case: Multiple navigation paths to the same entity need to resolve to a single instance.

AsSplitQuery

For queries with multiple includes, split into separate SQL queries:

public class OrderWithDetailsSpec : Specification<Order>
{
    public OrderWithDetailsSpec()
    {
        Query
            .Include(o => o.Customer)
            .Include(o => o.OrderItems)
                .ThenInclude(oi => oi.Product)
            .Include(o => o.ShippingAddress)
            .AsSplitQuery(); // Prevents cartesian explosion
    }
}

Benefits:

  • Avoids cartesian explosion in SQL
  • Better performance with multiple collection includes
  • Reduces data duplication over the wire

Generated SQL:

-- Query 1: Main entity
SELECT * FROM Orders WHERE ...

-- Query 2: First include
SELECT * FROM Customers WHERE OrderId IN (...)

-- Query 3: Second include
SELECT * FROM OrderItems WHERE OrderId IN (...)

IgnoreQueryFilters

Bypass global query filters when needed:

public class AllCustomersIncludingSoftDeletedSpec : Specification<Customer>
{
    public AllCustomersIncludingSoftDeletedSpec()
    {
        Query
            .IgnoreQueryFilters() // Bypass soft-delete filters, etc.
            .OrderBy(c => c.Name);
    }
}

๐ŸŽจ Complete Example

Domain Model

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public bool IsActive { get; set; }
    public bool IsPremium { get; set; }
    public decimal TotalPurchases { get; set; }
    public List<Order> Orders { get; set; } = new();
}

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public Customer Customer { get; set; }
    public DateTime OrderDate { get; set; }
    public decimal Total { get; set; }
    public List<OrderItem> OrderItems { get; set; } = new();
}

Specifications

// Active customers
public class ActiveCustomersSpec : Specification<Customer>
{
    public ActiveCustomersSpec()
    {
        Query
            .Where(c => c.IsActive)
            .OrderBy(c => c.Name)
            .AsNoTracking();
    }
}

// Paginated customer search
public class CustomerSearchSpec : Specification<Customer>
{
    public CustomerSearchSpec(string searchTerm, int page, int pageSize)
    {
        Query
            .Where(c => c.IsActive)
            .Search(c => c.Name, $"%{searchTerm}%")
            .Search(c => c.Email, $"%{searchTerm}%")
            .OrderBy(c => c.Name)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Include(c => c.Orders)
            .AsNoTracking();
    }
}

// Premium customers with orders
public class PremiumCustomersWithOrdersSpec : Specification<Customer>
{
    public PremiumCustomersWithOrdersSpec()
    {
        Query
            .Where(c => c.IsPremium && c.IsActive)
            .Include(c => c.Orders)
                .ThenInclude(o => o.OrderItems)
            .OrderByDescending(c => c.TotalPurchases)
            .AsNoTracking()
            .AsSplitQuery();
    }
}

// Customer by ID (single result)
public class CustomerByIdSpec : SingleResultSpecification<Customer>
{
    public CustomerByIdSpec(int id)
    {
        Query
            .Where(c => c.Id == id)
            .Include(c => c.Orders)
            .AsNoTracking();
    }
}

// Customer names projection
public class CustomerNamesSpec : Specification<Customer, string>
{
    public CustomerNamesSpec()
    {
        Query
            .Where(c => c.IsActive)
            .Select(c => c.Name)
            .OrderBy(name => name);
    }
}

Repository

public class CustomerRepository : RepositoryBase<Customer>
{
    public CustomerRepository(AppDbContext context) : base(context)
    {
    }
}

Service Layer

public class CustomerService
{
    private readonly CustomerRepository _repository;

    public CustomerService(CustomerRepository repository)
    {
        _repository = repository;
    }

    public async Task<List<Customer>> GetActiveCustomersAsync(CancellationToken ct)
    {
        return await _repository.ListAsync(new ActiveCustomersSpec(), ct);
    }

    public async Task<List<Customer>> SearchCustomersAsync(
        string searchTerm, int page, int pageSize, CancellationToken ct)
    {
        var spec = new CustomerSearchSpec(searchTerm, page, pageSize);
        return await _repository.ListAsync(spec, ct);
    }

    public async Task<Customer?> GetCustomerByIdAsync(int id, CancellationToken ct)
    {
        var spec = new CustomerByIdSpec(id);
        return await _repository.FirstOrDefaultAsync(spec, ct);
    }

    public async Task<int> CountActiveCustomersAsync(CancellationToken ct)
    {
        return await _repository.CountAsync(new ActiveCustomersSpec(), ct);
    }

    public async Task<bool> HasPremiumCustomersAsync(CancellationToken ct)
    {
        return await _repository.AnyAsync(
            new Specification<Customer>() { Query = q => q.Where(c => c.IsPremium) }, 
            ct
        );
    }

    public async Task<List<string>> GetActiveCustomerNamesAsync(CancellationToken ct)
    {
        return await _repository.ListAsync(new CustomerNamesSpec(), ct);
    }
}

Dependency Injection Setup

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Register DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Register repositories
builder.Services.AddScoped<CustomerRepository>();

// Register services
builder.Services.AddScoped<CustomerService>();

// Optional: Use cached evaluator for better performance
builder.Services.AddSingleton<ISpecificationEvaluator>(
    SpecificationEvaluator.Cached
);

var app = builder.Build();

๐Ÿงช Testing

Integration Testing with In-Memory Database

public class CustomerRepositoryTests
{
    private AppDbContext CreateInMemoryContext()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;

        return new AppDbContext(options);
    }

    [Fact]
    public async Task ListAsync_WithActiveCustomersSpec_ReturnsOnlyActiveCustomers()
    {
        // Arrange
        await using var context = CreateInMemoryContext();
        context.Customers.AddRange(
            new Customer { Id = 1, Name = "Alice", IsActive = true },
            new Customer { Id = 2, Name = "Bob", IsActive = false },
            new Customer { Id = 3, Name = "Charlie", IsActive = true }
        );
        await context.SaveChangesAsync();

        var repository = new CustomerRepository(context);
        var spec = new ActiveCustomersSpec();

        // Act
        var result = await repository.ListAsync(spec, CancellationToken.None);

        // Assert
        Assert.Equal(2, result.Count);
        Assert.All(result, c => Assert.True(c.IsActive));
    }

    [Fact]
    public async Task FirstOrDefaultAsync_WithCustomerByIdSpec_ReturnsCorrectCustomer()
    {
        // Arrange
        await using var context = CreateInMemoryContext();
        var expectedCustomer = new Customer { Id = 1, Name = "Alice", IsActive = true };
        context.Customers.Add(expectedCustomer);
        await context.SaveChangesAsync();

        var repository = new CustomerRepository(context);
        var spec = new CustomerByIdSpec(1);

        // Act
        var result = await repository.FirstOrDefaultAsync(spec, CancellationToken.None);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Alice", result.Name);
    }
}

๐Ÿ“Š Performance Comparison

Include Caching Impact

BenchmarkDotNet v0.13.x, Windows 11
Intel Core i7, 16GB RAM

|                     Method |     Mean |    Error |   StdDev |
|--------------------------- |---------:|---------:|---------:|
|   Default_IncludeEvaluator | 125.3 ฮผs |  2.45 ฮผs |  2.29 ฮผs |
|    Cached_IncludeEvaluator |  42.1 ฮผs |  0.83 ฮผs |  0.77 ฮผs |

Recommendation: Use SpecificationEvaluator.Cached for production applications with complex includes.


๐Ÿšจ Common Pitfalls

โŒ Don't: Mix AsTracking Flags

// DON'T: Multiple tracking configurations
Query
    .AsNoTracking()
    .AsTracking(); // โŒ Last one wins, but confusing

โŒ Don't: Use Includes for Count/Any

// DON'T: Includes are wasted in count queries
var count = await repository.CountAsync(
    new Specification<Customer>() 
    { 
        Query = q => q
            .Where(c => c.IsActive)
            .Include(c => c.Orders) // โŒ Unnecessary
    }, 
    ct
);

The repository automatically excludes includes for Count/Any operations.

โœ… Do: Use Appropriate Specification Types

// โœ… Use SingleResultSpecification for single entity queries
public class CustomerByIdSpec : SingleResultSpecification<Customer>
{
    public CustomerByIdSpec(int id)
    {
        Query.Where(c => c.Id == id);
    }
}

โœ… Do: Use AsSplitQuery for Multiple Includes

// โœ… Prevents cartesian explosion
Query
    .Include(o => o.Customer)
    .Include(o => o.OrderItems)
    .Include(o => o.ShippingAddress)
    .AsSplitQuery(); // Much better performance

๐Ÿ“– API Reference

SpecificationEvaluator

Property/Method Description
Default Singleton with default evaluators (no caching)
Cached Singleton with include delegate caching enabled
GetQuery<T>(query, spec) Apply specification to IQueryable<T>
GetQuery<T, TResult>(query, spec) Apply projection specification

Repository Methods (IRepositoryBase<T>)

Method Description Optimizations
ListAsync(spec, ct) Get all entities None
FirstOrDefaultAsync(spec, ct) Get first or null None
SingleOrDefaultAsync(spec, ct) Get single or null SingleResultSpecification only
CountAsync(spec, ct) Count entities Excludes includes/projections
AnyAsync(spec, ct) Check existence Excludes includes/projections
AddAsync(entity, ct) Add entity Change tracking enabled
UpdateAsync(entity, ct) Update entity Change tracking enabled
DeleteAsync(entity, ct) Delete entity Change tracking enabled
SaveChangesAsync(ct) Persist changes N/A

DbSet Extension Methods

Method Description
WithSpecification(spec) Apply specification to IQueryable
ToListAsync(spec, ct) Materialize with post-processing
ToEnumerableAsync(spec, ct) Lazy enumeration with post-processing
Search(criteria) Apply EF.Functions.Like search

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.


Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
26.2.11.1 89 2/12/2026