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 = await dapperService.QueryAsync<User>(dapperQuery);      // parameterized SQL
QueryResult<User> b = await db.Users.ToQueryResultAsync<User>(query);         // EF Core
QueryResult<User> c = cachedUsers.ToQueryResult(query);                       // in-memory

The packages

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.

Start here

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

The whole wiki

Foundations Getting Started · Architecture · Query Model · Query Semantics · Results and Metadata · JSON Contract · Fluent Builders

Behaviour Grouping and Hierarchies · Validation · Security · Cross-Provider Parity

Providers Dapper Provider · Dapper: Generated SQL · Dapper: Dialects · EF Core Provider · EF Core: Joins and Includes · In-Memory Provider

Practice Recipes · Sample Application · Testing · Extending QueryForge

Reference API 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.
var query = QueryBuilder.New()
    .Where(new QueryCriteria(
        logic: Logic.And,
        groups:
        [
            new ConditionGroup(
                logic: Logic.Or,
                conditions:
                [
                    new Condition("Country", ConditionOperator.Equals, "Germany"),
                    new Condition("Country", ConditionOperator.Equals, "Canada")
                ]),
            new ConditionGroup([ new Condition("Score", ConditionOperator.GreaterThan, 50) ])
        ]))
    .Select("UserId", "FirstName", "Country", "Score")
    .Sort(new SortDescriptor("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.
var viaEfCore   = await db.Users.AsNoTracking().ToQueryResultAsync<User>(query);
var viaMemory   = cachedUsers.ToQueryResult(query);
var viaDapper   = await svc.QueryAsync<User>(
    DapperQueryBuilder.FromBase(query).ForObject("Users", "dbo").Build());

// 4. Same contract from all three.
Console.WriteLine(viaDapper.Meta.Total.Rows);   // matching rows, before paging
Console.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