The developer-friendly Dynamic API Query Toolkit for .NET.
Add filtering, searching, sorting, field selection, pagination, and query optimization to any REST API — with a handful of allowlist declarations and one line per request.
var result = await db.Users
.Filter(request)
.Search(request.Search, config)
.Sort(request.Sort)
.PaginateAsync(request);GET /users?filter=age gt 18 and country eq "US"&sort=-createdAt&search=jane&page=1&pageSize=25
Every API ends up reinventing "let the client filter/sort/page the list" — usually as a pile of if (request.Sort == "...") branches that quietly become a SQL injection or over-fetching risk. QueryFlow gives you that capability as a small set of composable IQueryable<T> extension methods, backed by an explicit allowlist so clients can only touch fields and operators you've approved.
- ORM-independent core.
QueryFlow.Coreworks against anyIQueryable<T>— EF Core, an in-memoryList<T>.AsQueryable(), anything.QueryFlow.EFCoreadds eager-loading;QueryFlow.Dapperadds safe, parameterized SQL fragment building for raw-SQL codebases. - Secure by default. Every field name and operator is checked against a
QueryableConfig<T>allowlist before it touches an expression tree or a SQL string. Page sizes and clause counts are capped. Nothing gets string-concatenated into SQL. - Real cursor pagination. Not "base64(offset)" — genuine keyset pagination with HMAC-signed, tamper-evident cursors that stay correct under concurrent inserts/deletes, with bidirectional (next/previous) support.
- Performant. Reflection (property paths, generic method resolution) is cached; expression trees stay in a shape LINQ providers can translate to SQL, including field selection, which projects onto a runtime-emitted type so EF Core only selects the columns you asked for — not the whole row.
- Minimal setup. An unconfigured
QueryableConfig<T>allows every public property with type-appropriate operators. You only write an allowlist when you want to restrict something.
.NET 8.0, 9.0, or 10.0.
| Package | What it's for |
|---|---|
QueryFlow.Abstractions |
Request/response models, enums, exceptions, options — no dependencies. |
QueryFlow.Expressions |
Expression-tree builders for filter/sort/search/select/cursor-seek/aggregate, with cached reflection. |
QueryFlow.Validation |
QueryableConfig<T> allowlist builder + request validator. |
QueryFlow.Core |
The main developer-facing API: Filter, Search, Sort, SelectFields, Paginate(Async), CursorPaginate(Async), AggregateAsync. |
QueryFlow.EFCore |
Include() eager loading + ApplyQuery() one-call composition for EF Core. |
QueryFlow.Dapper |
DapperQueryBuilder<T> — safe, parameterized WHERE/ORDER BY/paging SQL fragments for raw-SQL codebases. |
QueryFlow.AspNetCore |
Query-string DSL parser, minimal-API model binding, DI registration, exception → ProblemDetails middleware. |
QueryFlow.OpenApi |
Swashbuckle operation filter documenting the standard query parameters. |
Install what you need:
dotnet add package QueryFlow.Core
dotnet add package QueryFlow.EFCore # if you're on EF Core
dotnet add package QueryFlow.Dapper # if you're on Dapper
dotnet add package QueryFlow.AspNetCore # query-string binding + DI
dotnet add package QueryFlow.OpenApi # optional, Swagger docs1. Declare what clients are allowed to query:
public static class UserQueries
{
public static readonly QueryableConfig<User> Config = new QueryableConfig<User>()
.AllowFilter(u => u.Age, FilterOperator.GreaterThan, FilterOperator.Equals)
.AllowFilter(u => u.Country, FilterOperator.Equals, FilterOperator.In)
.AllowSort(u => u.CreatedAt)
.AllowSort(u => u.Name)
.Searchable(u => u.Name)
.Searchable(u => u.Email);
}2. Wire up an endpoint:
app.MapGet("/users", async (QueryRequestParameter q, AppDbContext db) =>
{
var request = q.Value;
var query = db.Users.ApplyQuery(request, UserQueries.Config); // Filter + Search + Sort
var page = await query.PaginateAsync(request);
return Results.Ok(page);
});3. Query it:
GET /users?filter=age gt 18 and country eq "US"&sort=-createdAt&search=jane&page=1&pageSize=25
That's it — see samples/QueryFlow.Samples.WebApi for a complete, runnable example (books/authors API) covering filtering, search, sorting, field selection, both pagination styles, aggregates, and eager loading.
Equals · NotEquals · GreaterThan · GreaterThanOrEqual · LessThan · LessThanOrEqual · Between · In · NotIn · Contains · StartsWith · EndsWith · IsNull · IsNotNull
GET /users?filter=age gt 18
{ "filters": [ { "field": "Age", "operator": "GreaterThan", "value": 18 } ] }Nested AND/OR groups are supported via FilterGroup for cases the flat query-string DSL can't express.
GET /users?sort=LastName,-CreatedDate
GET /users?search=john
Matches whichever fields you registered with .Searchable(...).
GET /users?page=3&pageSize=25
GET /users?cursor=eyJrZXlzIjpb...&pageSize=25
Cursors are opaque, base64url-encoded, HMAC-signed keyset tokens — not an offset in disguise. They stay correct as rows are inserted/deleted around the page boundary, and support paging backward via previousCursor.
GET /users?fields=id,name,email
Projects down to just the requested columns — for EF Core, that's a real SELECT id, name, email, not a full-row fetch trimmed in memory.
GET /orders?include=customer,address
Count · Sum · Average · Max · Min, computed server-side via AggregateAsync.
- Field names and operators are checked against your
QueryableConfig<T>allowlist before any expression tree or SQL string is built. QueryFlowOptions.MaxPageSize/MaxFilterConditions/MaxSortFields/MaxIncludeDepthcap request size.- Dapper SQL fragments use bound parameters exclusively; identifiers are re-validated against a strict pattern before being spliced into SQL text.
- Cursor tokens are HMAC-signed — clients can't forge one to skip validation or see data outside their filtered view.
- Quick Start
- Installation
- Architecture Overview
- API Reference
- Migration Guide
- FAQ
- Contributing
- Code of Conduct
- Changelog
Run dotnet run -c Release --project benchmarks/QueryFlow.Benchmarks for BenchmarkDotNet results on filtering, sorting, pagination and field selection at 1K/100K rows.
Contributions are welcome — see CONTRIBUTING.md. Issues labeled good first issue are a good place to start.
MongoDB / Cosmos DB / Elasticsearch providers · OData compatibility · GraphQL support · Specification Pattern integration · Blazor helpers · OpenTelemetry instrumentation · query analytics · source generators for compile-time allowlists.