QChain.EntityFrameworkCore 1.0.0

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

NuGet - QChain NuGet - QChain.EntityFrameworkCore Tests License


QChain

LINQ specification pattern for building reusable and composable DAL query pipelines.

QChain lets you build reusable, composable, and expressive query pipelines on top of LINQ.
Instead of duplicating query logic across repositories and services, you define query fragments once and chain them together.


✨ Motivation

LINQ is powerful, but in real-world applications it often leads to:

  • duplicated query logic
  • hard-to-read query chains
  • bloated repositories
  • poor support for reusable specifications

What QChain Solves

  • Reusable predicates
  • Composable pipelines
  • Flexible construction and execution

👎 Before QChain

Large EF Core applications often end up with long methods, and duplicated, tightly coupled query logic.

  • Joins produce anonymous intermediate types, making query composition and reuse difficult.
  • Mapping is either baked in or deferred until after execution, requiring full entities to be loaded.
  • Pagination, sorting, or extra filters require more repository methods and a wider API surface.
public Task<List<CustomerBalanceDto>> GetActiveEuropeanCustomerBalancesAsync(DateTime from, CancellationToken ct)
{
    return db.Customers
        .Where(c => c.IsActive && c.Region == "EU")                                     // duplicated predicates
        .Join(db.Orders, c => c.Id, o => o.CustomerId, (c, o) => new { c, o })          // anonymous<Customer, Order>
        .Join(db.Payments, x => x.o.Id, p => p.OrderId, (x, p) => new { x.c, x.o, p })  // anonymous<Customer, Order, Payment>
        .Where(x => x.o.CreatedAt >= from)
        .Select(x => new CustomerBalanceDto(x.c.Id, x.c.Name, x.p.Amount))  // mapping baked into DAL layer
        .ToArrayAsync(ct);                                                  // no pagination support
}

public Task<List<CustomerRiskDto>> GetRecentEuropeanCustomerRisksAsync(DateTime from, CancellationToken ct)
{
    return db.Customers
        .Where(c => c.IsActive && c.Region == "EU")                                     // duplicated predicates
        .Join(db.Orders, c => c.Id, o => o.CustomerId, (c, o) => new { c, o })          // anonymous<Customer, Order>
        .Join(db.Payments, x => x.o.Id, p => p.OrderId, (x, p) => new { x.c, x.o, p })  // anonymous<Customer, Order, Payment>
        .Where(x => x.o.CreatedAt >= from)
        .Where(x => x.p.Amount >= 10000)
        .Select(x => new CustomerRiskDto(x.c.Id, x.c.Name, risk: "High"))  // mapping baked into DAL layer
        .ToArrayAsync(ct);                                                 // no pagination support
}

👍 With QChain

Readable, reusable, and aligned with your domain. QChain keeps intermediate query shapes as named tuples instead of anonymous types.

public IQuery<(Customer c, Order o, Payment p)> GetActiveEuropeanCustomerBalances(DateTime from)
{
    return db.Customers
        .Where(c => c.IsActive().And(c.FromEurope()))  // composable predicates
        .WithOrders(db.Orders.CreatedAfter(from))      // Tuple<(Customer c, Order o)>
        .WithPayments();                               // Tuple<(Customer c, Order o, Payment p)>
}

public IQuery<(Customer c, Order o, Payment p)> GetRecentEuropeanCustomerRisks(DateTime from)
{
    return db.Customers
        .Where(c => c.IsActive().And(c.FromEurope()))  // composable predicates
        .WithOrders(db.Orders.CreatedAfter(from))      // Tuple<(Customer c, Order o)>
        .WithPayments(db.Payments.AmountOver(10000));  // Tuple<(Customer c, Order o, Payment p)>
}

🔗 Composing at the Call Site

Mapping and pagination compose externally. Query composition is reusable while execution concerns remain composable.

var balances = await unitOfWork.Query(db => db.Customers
        .GetActiveEuropeanCustomerBalances(from)
        .Select(x => new CustomerBalanceDto(x.c.Id, x.c.Name, x.p.Amount))  // mapping remains at the calling layer
        .Page(index, size))                                                 // pagination is applied as a query extension 
    .ToArrayAsync(ct);

var risks = await unitOfWork.Query(db => db.Customers
        .GetRecentEuropeanCustomerRisks(from)
        .Select(x => new CustomerRiskDto(x.c.Id, x.c.Name, risk: "High"))  // mapping remains at the calling layer
        .Page(index, size))                                                // pagination is applied as a query extension 
    .ToArrayAsync(ct);

🏗️ Basic Usage

Start from any IQueryable<T> and wrap it in a Query<T>.

IQuery<Account> Accounts = new Query<Account>(db.Set<Account>());
IQuery<Order> Orders = new Query<Order>(db.Set<Order>());

Define reusable predicates as extension methods over the entity type.

public static class AccountPredicates
{
    public static Expression<Func<Account, bool>> IsActive(this Account _)
        => account => account.IsActive;

    public static Expression<Func<Account, bool>> FromEurope(this Account _)
        => account => account.Region == Region.Europe;
}

public static class OrderPredicates
{
    public static Expression<Func<Order, bool>> InLastMonth(this Order _)
        => order => order.CreatedDate >= DateTime.UtcNow.AddMonths(-1);

    public static Expression<Func<Order, bool>> InEuro(this Order _)
        => order => order.CurrencyId == CurrencyType.EUR;
}

Unit of Work

Expose query roots from your unit of work as IQuery<T> and provide a small set of Query overloads for invoking query logic inside that unit of work boundary. QueryExecutor<T> is provided for deferred execution.

public sealed class UnitOfWork(AppDbContext db) : IUnitOfWork
{
    public IQuery<Account> Accounts { get; } = new Query<Account>(db.Set<Account>());
    public IQuery<Order> Orders { get; } = new Query<Order>(db.Set<Order>());

    public IAccountsRepository AccountsRepository { get; } = new AccountsRepository(this);

    public T Query<T>(Func<IUnitOfWork, T> query) => query(this);

    public Task<T> Query<T>(Func<IUnitOfWork, Task<T>> query) => query(this);

    public IQueryExecutor<T> Query<T>(Func<IUnitOfWork, IQuery<T>> query) =>
        new QueryExecutor<T>(query(this));
}
  • Query<T>(Func<IUnitOfWork, T>) for immediate values
  • Query<T>(Func<IUnitOfWork, Task<T>>) for async work
  • Query<T>(Func<IUnitOfWork, IQuery<T>>) for deferred query execution
int totalAccounts = unitOfWork.Query(db => db.Accounts.Count());

int activeCount = await unitOfWork.Query(db => db.Accounts
        .Where(a => a.IsActive())
        .CountAsync());

(Account account, Order order)[] ordersInLastMonth = await unitOfWork.Query(db =>
        db.AccountsRepository.ActiveEuropeanOrdersInLastMonth())
    .ToArrayAsync();
public class AccountsRepository(IUnitOfWork db)
{
    public IQuery<(Account account, Order order)> ActiveEuropeanOrdersInLastMonth() =>
        db.Accounts
            .Join(db.Orders, a => a.AccountId, o => o.AccountId,
                (a, o) => ValueTuple.Create(a, o))
            // predicates are reusable across joins
            .Where(x => x.account.IsActive().And(x.order.InLastMonth())); 
}

Escaping the Abstraction

AsQueryable() exposes the underlying IQueryable<T> for direct LINQ provider access.

This is when QChain abstractions are insufficient or translation fails.

var sql = query
    .AsQueryable()
    .ToQueryString();

📦 Packages

  • QChain - Core abstractions and query pipeline
  • QChain.EntityFrameworkCore - EF Core integration

🔧 Installation

dotnet add package QChain

For EF Core support:

dotnet add package QChain.EntityFrameworkCore

Feedback/Contribution

Issues, Discussions, and Pull Requests are welcome.

Product Compatible and additional computed target framework versions.
.NET 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
1.0.0 89 5/27/2026