Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
Home
![]()
A provider-agnostic dynamic query engine for .NET.
Describe a query once — filters, sorting, paging, grouping — and execute it against SQL Server,
PostgreSQL, MySQL/MariaDB, Oracle, SQLite, anything EF Core can reach, or a plain in-memory list.
QueryForge separates what you want from how it is fetched.
A Query is a plain, serializable description of intent: which rows, in which order, which page,
grouped how, projecting which columns. It contains no SQL, no LINQ, and no reference to a database.
An execution provider takes that intent and runs it — compiling parameterized SQL, building an
EF Core expression tree, or evaluating it over an IEnumerable<T>.
The value of the split is that the same request works everywhere and answers identically. A filter payload posted by a data grid, a report built in C#, and a unit test over a hard-coded list all use one model and one result contract.
// The same Query object, three execution providers, one result shape.QueryResult<User>a=awaitdapperService.QueryAsync<User>(dapperQuery);// parameterized SQLQueryResult<User>b=awaitdb.Users.ToQueryResultAsync<User>(query);// EF CoreQueryResult<User>c=cachedUsers.ToQueryResult(query);// in-memory| Package | Role | Depends on |
|---|---|---|
| PepperX.QueryForge | The query model, fluent builder, validation engine, and the shared execution semantics every provider obeys. No dependencies, runs no SQL. | — |
| PepperX.QueryForge.Dapper | Compiles a query into parameterized SQL and executes it with Dapper. Five engines from one codebase. Nothing is deployed to your database. | Dapper |
| PepperX.QueryForge.EFCore | Translates a query into expression trees so EF Core generates the SQL, honouring your model, global filters and value converters. | EF Core 10 |
| PepperX.QueryForge.InMemory | Runs a query over any IEnumerable<T>. Cached data, composed API results, test doubles. | — |
All four target .NET 10, are versioned 2.0.0, and are MIT licensed.
| If you want to… | Read |
|---|---|
| Install a package and run your first query | Getting Started |
| Understand how the pieces fit together | Architecture |
| Know exactly what every field of a query means | Query Model |
| Know exactly how a query is evaluated — the specification | Query Semantics |
| Accept a query from a browser or mobile client | JSON Contract |
| Stop a client reaching a column it should not | Validation and Security |
Build nested key / count / items trees | Grouping and Hierarchies |
| See the SQL that actually runs | Dapper: Generated SQL |
| Join tables, eager-load, or use split queries | EF Core: Joins and Includes |
| Add a database engine QueryForge does not ship | Extending QueryForge |
| Look up a type or method signature | API Reference |
| Understand an exception you just hit | Error Reference |
| Upgrade from QueryForge 1.x | Migration: 1.x to 2.0 |
FoundationsGetting Started · Architecture · Query Model · Query Semantics · Results and Metadata · JSON Contract · Fluent Builders
BehaviourGrouping and Hierarchies · Validation · Security · Cross-Provider Parity
ProvidersDapper Provider · Dapper: Generated SQL · Dapper: Dialects · EF Core Provider · EF Core: Joins and Includes · In-Memory Provider
PracticeRecipes · Sample Application · Testing · Extending QueryForge
ReferenceAPI Reference · Error Reference · Migration: 1.x to 2.0 · Release Process · FAQ
// 1. Describe the intent. This object is serializable and provider-free.varquery=QueryBuilder.New().Where(newQueryCriteria(logic:Logic.And,groups:[newConditionGroup(logic:Logic.Or,conditions:[newCondition("Country",ConditionOperator.Equals,"Germany"),newCondition("Country",ConditionOperator.Equals,"Canada")]),newConditionGroup([newCondition("Score",ConditionOperator.GreaterThan,50)])])).Select("UserId","FirstName","Country","Score").Sort(newSortDescriptor("Score",SortOrder.Descending)).Page(size:20,number:1).Build();// 2. Constrain what a caller is allowed to ask for.query.Validate(rules =>{rules.Select(c =>c.Deny("PasswordHash"));rules.PageSize(p =>p.Max(100));},QueryValidationMode.SilentStrip);// 3. Execute it. Pick one.varviaEfCore=awaitdb.Users.AsNoTracking().ToQueryResultAsync<User>(query);varviaMemory=cachedUsers.ToQueryResult(query);varviaDapper=awaitsvc.QueryAsync<User>(DapperQueryBuilder.FromBase(query).ForObject("Users","dbo").Build());// 4. Same contract from all three.Console.WriteLine(viaDapper.Meta.Total.Rows);// matching rows, before pagingConsole.WriteLine(viaDapper.Models.Count);// rows on this pageThese are the promises the test suites are written to enforce. They are described in full under Cross-Provider Parity.
- One
Queryin, oneQueryResult<T>out — the same request returns the same answer from every provider, including group counts, null placement, and page boundaries. - Values are parameters, never inlined text. Comparisons use the column's real type, so
Age > 9is numeric rather than lexical, and databases can cache a plan. - Column names are checked against what the target actually exposes. Anything unrecognised is dropped rather than escaped and emitted, so a filter payload cannot be used to probe your schema.
- Nothing is deployed. No stored procedures, no DDL permissions, no startup migration step.
- An unusable filter is dropped, not turned into a match-nothing clause. A filter the caller never filled in must not silently empty the result.
QueryForge · part of the PepperX Ecosystem · MIT licensed · packages 2.0.0, .NET 10