Skip to content
AmirHosseinMp02 edited this page Aug 2, 2026 · 3 revisions

QueryForge

QueryForge Wiki

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.


What QueryForge is

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

The packages

PackageRoleDepends on
PepperX.QueryForgeThe query model, fluent builder, validation engine, and the shared execution semantics every provider obeys. No dependencies, runs no SQL.
PepperX.QueryForge.DapperCompiles a query into parameterized SQL and executes it with Dapper. Five engines from one codebase. Nothing is deployed to your database.Dapper
PepperX.QueryForge.EFCoreTranslates a query into expression trees so EF Core generates the SQL, honouring your model, global filters and value converters.EF Core 10
PepperX.QueryForge.InMemoryRuns 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.

Start here

If you want to…Read
Install a package and run your first queryGetting Started
Understand how the pieces fit togetherArchitecture
Know exactly what every field of a query meansQuery Model
Know exactly how a query is evaluated — the specificationQuery Semantics
Accept a query from a browser or mobile clientJSON Contract
Stop a client reaching a column it should notValidation and Security
Build nested key / count / items treesGrouping and Hierarchies
See the SQL that actually runsDapper: Generated SQL
Join tables, eager-load, or use split queriesEF Core: Joins and Includes
Add a database engine QueryForge does not shipExtending QueryForge
Look up a type or method signatureAPI Reference
Understand an exception you just hitError Reference
Upgrade from QueryForge 1.xMigration: 1.x to 2.0

The whole wiki

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

A sixty-second tour

// 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 page

Design commitments

These are the promises the test suites are written to enforce. They are described in full under Cross-Provider Parity.

  • One Query in, one QueryResult<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 > 9 is 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.

Clone this wiki locally