DesignPatterns.Specification.Net 26.2.11.1

dotnet add package DesignPatterns.Specification.Net --version 26.2.11.1
                    
NuGet\Install-Package DesignPatterns.Specification.Net -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" 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" Version="26.2.11.1" />
                    
Directory.Packages.props
<PackageReference Include="DesignPatterns.Specification.Net" />
                    
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 --version 26.2.11.1
                    
#r "nuget: DesignPatterns.Specification.Net, 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@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&version=26.2.11.1
                    
Install as a Cake Addin
#tool nuget:?package=DesignPatterns.Specification.Net&version=26.2.11.1
                    
Install as a Cake Tool

DesignPatterns.Specification.Net

NuGet License: MIT

A lightweight, fluent Specification Pattern library for .NET that helps you build reusable, composable, and testable query logic. Works seamlessly with Entity Framework Core and in-memory collections.


๐Ÿ“ฆ Installation

# Core library
dotnet add package DesignPatterns.Specification.Net

# Entity Framework Core integration
dotnet add package DesignPatterns.Specification.Net.EntityFrameworkCore

๐ŸŽฏ Why Use This Library?

The Specification pattern helps you:

  • Encapsulate query logic in reusable, testable classes
  • Eliminate duplicate queries across your application
  • Compose complex queries using fluent, readable syntax
  • Separate concerns by keeping query logic out of repositories and services
  • Test easily with in-memory evaluation
  • Apply consistently to both EF Core and in-memory collections

๐Ÿš€ Quick Start

1. Define a Specification

using DesignPatterns.Specification.Net;

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

2. Use with Entity Framework Core

// Apply specification to DbSet
var activeCustomers = await dbContext.Customers
    .WithSpecification(new ActiveCustomersSpec())
    .ToListAsync(cancellationToken);

// Or use the direct extension
var activeCustomers = await dbContext.Customers
    .ToListAsync(new ActiveCustomersSpec(), cancellationToken);

3. Use with In-Memory Collections

var customers = new List<Customer> { /* ... */ };

// Evaluate specification against in-memory data
var activeCustomers = new ActiveCustomersSpec().Evaluate(customers);

๐ŸŽจ Features

โœ… Fluent Query Builder

Build complex queries with an intuitive fluent API:

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)
            .ThenInclude(o => o.OrderItems)
            .AsNoTracking();
    }
}

โœ… Projection Support

Transform query results with type-safe projections:

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

// Usage
var names = await dbContext.Customers
    .ToListAsync(new CustomerNamesSpec(), cancellationToken);

โœ… Single Result Queries

Optimize queries that return a single result:

public class CustomerByIdSpec : SingleResultSpecification<Customer>
{
    public CustomerByIdSpec(int id)
    {
        Query
            .Where(c => c.Id == id)
            .Include(c => c.Orders);
    }
}

// Usage
var customer = await repository.FirstOrDefaultAsync(
    new CustomerByIdSpec(customerId), 
    cancellationToken
);

โœ… Advanced EF Core Features

public class OptimizedOrdersSpec : Specification<Order>
{
    public OptimizedOrdersSpec()
    {
        Query
            .Where(o => o.Total > 1000)
            .Include(o => o.Customer)
            .Include(o => o.OrderItems)
            .AsNoTracking()                          // No change tracking
            .AsSplitQuery()                          // Split into multiple SQL queries
            .IgnoreQueryFilters();                   // Ignore global query filters
    }
}

โœ… Search with Pattern Matching

Supports SQL LIKE patterns (%, _, [A-Z], [^...]):

public class CustomerSearchSpec : Specification<Customer>
{
    public CustomerSearchSpec(string pattern)
    {
        Query.Search(c => c.Name, pattern);
        // Translates to EF.Functions.Like in EF Core
        // Uses custom Like extension for in-memory collections
    }
}

โœ… Post-Processing Actions

Execute logic after materialization:

public class CustomersWithComputedDataSpec : Specification<Customer>
{
    public CustomersWithComputedDataSpec()
    {
        Query
            .Where(c => c.IsActive)
            .PostProcessingAction(customers => 
            {
                foreach (var customer in customers)
                {
                    customer.FullName = $"{customer.FirstName} {customer.LastName}";
                }
            });
    }
}

โœ… Caching Support

Enable caching for frequently used specifications:

public class CachedCustomersSpec : Specification<Customer>
{
    public CachedCustomersSpec()
    {
        Query
            .Where(c => c.IsActive)
            .EnableCache("ActiveCustomers", nameof(Customer));
    }
}

๐Ÿ“š Core Concepts

ISpecification<T>

The main interface that represents a specification for type T. Contains all query rules, options, and metadata.

Specification<T>

Base class for creating specifications. Use the Query property to build your specification using the fluent API.

SingleResultSpecification<T>

Specialized base class optimized for queries that return a single result (e.g., by ID or unique key).

Specification<T, TResult>

Base class for specifications with projections, transforming T to TResult.

SpecificationBuilder

Fluent API for composing query rules:

  • Where: Filter criteria
  • Include/ThenInclude: Eager loading
  • OrderBy/ThenBy/OrderByDescending/ThenByDescending: Sorting
  • Skip/Take: Pagination
  • Search: Pattern-based search
  • Select/SelectMany: Projection
  • PostProcessingAction: Post-materialization logic

๐Ÿ—„๏ธ Repository Integration

Using the Provided Repository Base Classes

// Entity Framework Core repository
public class CustomerRepository : RepositoryBase<Customer>
{
    public CustomerRepository(AppDbContext context) : base(context)
    {
    }
}

// Usage
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<Customer?> GetCustomerByIdAsync(int id, CancellationToken ct)
    {
        return await _repository.FirstOrDefaultAsync(new CustomerByIdSpec(id), ct);
    }

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

Using IDbContextFactory (for long-lived services)

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

๐Ÿ”ง Advanced Scenarios

Combining Multiple Specifications

public class ActivePremiumCustomersSpec : Specification<Customer>
{
    public ActivePremiumCustomersSpec()
    {
        Query
            .Where(c => c.IsActive)
            .Where(c => c.IsPremium)
            .Where(c => c.TotalPurchases > 10000);
    }
}

Dynamic Specifications

public class CustomerFilterSpec : Specification<Customer>
{
    public CustomerFilterSpec(CustomerFilter filter)
    {
        if (filter.IsActive.HasValue)
            Query.Where(c => c.IsActive == filter.IsActive.Value);

        if (!string.IsNullOrEmpty(filter.SearchTerm))
            Query.Search(c => c.Name, $"%{filter.SearchTerm}%");

        if (filter.MinPurchases.HasValue)
            Query.Where(c => c.TotalPurchases >= filter.MinPurchases.Value);

        Query
            .OrderBy(c => c.Name)
            .Skip((filter.Page - 1) * filter.PageSize)
            .Take(filter.PageSize);
    }
}

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)
            .AsNoTracking();
    }
}

Using Custom Evaluators

// Create a custom in-memory evaluator
var customEvaluator = new InMemorySpecificationEvaluator(
    whereEvaluator: new WhereEvaluator(),
    orderEvaluator: new OrderEvaluator(),
    paginationEvaluator: new PaginationEvaluator()
);

// Use in specification
public class CustomSpec : Specification<Customer>
{
    public CustomSpec() : base(customEvaluator)
    {
        Query.Where(c => c.IsActive);
    }
}

โšก Performance Tips

1. Use Cached Include Evaluator (EF Core)

For specifications with complex include chains:

// Configure in your DI container
services.AddSingleton<ISpecificationEvaluator>(
    SpecificationEvaluator.Cached
);

2. Disable Tracking When Appropriate

Query.AsNoTracking(); // For read-only queries

3. Use Split Queries for Large Includes

Query
    .Include(o => o.Customer)
    .Include(o => o.OrderItems)
    .AsSplitQuery(); // Generates multiple SQL queries

4. Optimize Count/Any Operations

Repositories automatically optimize these operations by excluding includes and projections.


๐Ÿงช Testing

Unit Testing with In-Memory Evaluation

[Fact]
public void ActiveCustomersSpec_ShouldFilterActiveCustomers()
{
    // Arrange
    var customers = new List<Customer>
    {
        new() { Id = 1, Name = "Alice", IsActive = true },
        new() { Id = 2, Name = "Bob", IsActive = false },
        new() { Id = 3, Name = "Charlie", IsActive = true }
    };

    var spec = new ActiveCustomersSpec();

    // Act
    var result = spec.Evaluate(customers).ToList();

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

Integration Testing with EF Core

[Fact]
public async Task CustomerRepository_WithActiveSpec_ReturnsActiveCustomers()
{
    // Arrange
    using var context = CreateInMemoryDbContext();
    await SeedTestDataAsync(context);
    var repository = new CustomerRepository(context);

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

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

๐Ÿšจ Common Pitfalls & Best Practices

โŒ Avoid: Multiple OrderBy Calls

// DON'T: This will throw DuplicateOrderChainException
Query
    .OrderBy(c => c.Name)
    .OrderBy(c => c.Email); // โŒ Wrong!
// DO: Use ThenBy for secondary ordering
Query
    .OrderBy(c => c.Name)
    .ThenBy(c => c.Email); // โœ… Correct!

โŒ Avoid: Multiple Skip or Take Calls

// DON'T: This will throw exceptions
Query.Skip(10).Skip(20);  // โŒ DuplicateSkipException
Query.Take(10).Take(20);  // โŒ DuplicateTakeException

โŒ Avoid: Using Includes for Count/Any

// DON'T: Includes are ignored in count operations anyway
public class CustomerCountSpec : Specification<Customer>
{
    public CustomerCountSpec()
    {
        Query
            .Where(c => c.IsActive)
            .Include(c => c.Orders); // โŒ Unnecessary for counting
    }
}
// DO: Keep count specs simple
public class ActiveCustomerCountSpec : Specification<Customer>
{
    public ActiveCustomerCountSpec()
    {
        Query.Where(c => c.IsActive); // โœ… Clean and efficient
    }
}

โœ… Do: Use AsNoTracking for Read-Only Queries

public class ReadOnlyCustomersSpec : Specification<Customer>
{
    public ReadOnlyCustomersSpec()
    {
        Query
            .Where(c => c.IsActive)
            .AsNoTracking(); // โœ… Better performance
    }
}

โœ… Do: Keep Specifications Focused

// โœ… Good: Single responsibility
public class ActiveCustomersSpec : Specification<Customer>
{
    public ActiveCustomersSpec()
    {
        Query.Where(c => c.IsActive);
    }
}

// โœ… Good: Compose when needed
public class ActivePremiumCustomersSpec : Specification<Customer>
{
    public ActivePremiumCustomersSpec()
    {
        Query
            .Where(c => c.IsActive)
            .Where(c => c.IsPremium);
    }
}

๐Ÿ“– API Reference

Query Builder Methods

Method Description
Where(predicate) Add filter criteria
OrderBy(keySelector) Primary ascending sort
OrderByDescending(keySelector) Primary descending sort
ThenBy(keySelector) Secondary ascending sort
ThenByDescending(keySelector) Secondary descending sort
Include(navigationProperty) Eager load related entity
ThenInclude(navigationProperty) Eager load nested related entity
Skip(count) Skip specified number of records
Take(count) Take specified number of records
Search(selector, pattern) Pattern-based search (LIKE)
Select(selector) Project to different type
SelectMany(selector) Flatten and project
PostProcessingAction(action) Execute after materialization
AsTracking() Enable change tracking (EF Core)
AsNoTracking() Disable change tracking (EF Core)
AsNoTrackingWithIdentityResolution() No tracking with identity resolution
AsSplitQuery() Use multiple SQL queries for includes
IgnoreQueryFilters() Ignore global query filters
EnableCache(key, args) Enable result caching

Repository Methods

Method Description
ListAsync(spec, ct) Get all matching entities
FirstOrDefaultAsync(spec, ct) Get first match or null
SingleOrDefaultAsync(spec, ct) Get single match or null
CountAsync(spec, ct) Count matching entities
AnyAsync(spec, ct) Check if any match exists
AddAsync(entity, ct) Add new entity
UpdateAsync(entity, ct) Update existing entity
DeleteAsync(entity, ct) Delete entity
SaveChangesAsync(ct) Persist changes

๐Ÿ“„ License

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


๐Ÿ™ Acknowledgments

This library provides a pragmatic, production-ready implementation of the Specification pattern for .NET, emphasizing:

  • Reusability: Define query logic once, use everywhere
  • Testability: Easy to test with in-memory evaluation
  • Maintainability: Clear separation of query concerns
  • Performance: Optimized for EF Core with caching options
  • Type Safety: Strongly-typed fluent API
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.
  • net10.0

    • No dependencies.
  • net6.0

    • No dependencies.
  • net7.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on DesignPatterns.Specification.Net:

Package Downloads
DesignPatterns.Specification.Net.EntityFrameworkCore

Entity Framework Core integration for DesignPatterns.Specification.Net. Provides EF Core-aware evaluators (Where, Include/ThenInclude with optional delegate caching, Order, Pagination), search via EF.Functions.Like, DbSet/IQueryable extensions (WithSpecification, ToListAsync/ToEnumerableAsync), and repository bases (RepositoryBase, ContextFactoryRepositoryBase, EFRepositoryFactory) with support for AsNoTracking/AsNoTrackingWithIdentityResolution, AsSplitQuery, and IgnoreQueryFilters.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
26.2.11.1 102 2/12/2026