QChain.EntityFrameworkCore
1.0.0
dotnet add package QChain.EntityFrameworkCore --version 1.0.0
NuGet\Install-Package QChain.EntityFrameworkCore -Version 1.0.0
<PackageReference Include="QChain.EntityFrameworkCore" Version="1.0.0" />
<PackageVersion Include="QChain.EntityFrameworkCore" Version="1.0.0" />
<PackageReference Include="QChain.EntityFrameworkCore" />
paket add QChain.EntityFrameworkCore --version 1.0.0
#r "nuget: QChain.EntityFrameworkCore, 1.0.0"
#:package QChain.EntityFrameworkCore@1.0.0
#addin nuget:?package=QChain.EntityFrameworkCore&version=1.0.0
#tool nuget:?package=QChain.EntityFrameworkCore&version=1.0.0
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 valuesQuery<T>(Func<IUnitOfWork, Task<T>>)for async workQuery<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 | Versions 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. |
-
net10.0
- Microsoft.EntityFrameworkCore (>= 10.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.0)
- QChain (>= 1.0.0)
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.0)
- QChain (>= 1.0.0)
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.0)
- QChain (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 89 | 5/27/2026 |