-
Notifications
You must be signed in to change notification settings - Fork 0
FAQ
The one for how you fetch data. The core comes along with any of them.
- Raw SQL over a relational database →
PepperX.QueryForge.Dapper - You already have a
DbContext→PepperX.QueryForge.EFCore - A list you already hold →
PepperX.QueryForge.InMemory
Installing more than one is normal. The sample uses all three.
Yes, including against the same table. They return the same QueryResult<T> and are asserted against
each other in the parity suite.
No. All four packages target .NET 10 only.
No. QueryForge is a read engine — there is no insert, update or delete surface, and none is planned.
Not in the query model — a query targets one object — but you compose the join before handing the
query over, which works fully. With EF Core, project the join into a named DTO and the joined columns
become filterable, sortable and groupable like any other. Full treatment, including Include and
AsSplitQuery, in EF Core: Joins and Includes.
await db.Orders
.Select(o => new OrderRow { Id = o.Id, CustomerName = o.Customer.Name, /* … */ })
.ToQueryResultAsync<OrderRow>(query);QueryForge then filters and sorts the flat shape.
Dotted paths do not resolve at all. The navigation itself ("Customer") does resolve as a filter —
Equals against null is a valid "has a related row" test — but it is never sortable or groupable.
Project to a flat DTO to query joined columns; see
EF Core: Joins and Includes.
It no longer does. EF Core drops every Include from a query carrying a projection, so QueryForge
detects the include and skips the projection instead — selectColumns is ignored on such a query
rather than the included data being silently lost. If you need both a narrowed payload and related
data, project the join into a flat DTO. See
EF Core: Joins and Includes.
Not in the model. Grouping produces counts and nested rows. For aggregates, put them in a view and query that, or compute them from a grouped result if the rows are already in hand.
Because page size bounds the number of groups, and every row under those groups comes back so that
Count can be truthful. Filter before grouping, or group by a higher-cardinality column. Full
explanation: Grouping and Hierarchies.
On a grouped result it counts distinct outermost group keys, not rows — because that is what is being paged. See Results and Metadata.
By design. An input QueryForge cannot honour is dropped, never turned into a match-nothing clause — a search box the user left blank means "no constraint", not "no results".
The checklist for finding out which rule dropped it is in Error Reference.
Same reason, plus a security one: an unknown column name is dropped silently so a caller cannot use error responses to discover your schema. See Security.
Your sort does not uniquely determine an order, so the database is free to break ties differently between calls. Add a unique tie-breaker:
.Sort(new SortDescriptor("Score", SortOrder.Descending), new SortDescriptor("UserId"))Almost always string case. In-memory comparison is OrdinalIgnoreCase; a database follows its
column's collation. The full list of divergences is in
Cross-Provider Parity.
The structural protections are automatic — values are parameters, unknown columns are dropped, and a client cannot choose the target. What you must add is policy: which real columns a caller may reach, and how large a page may be. That is Validation, and it is not optional on a public endpoint.
Not through the model. Query has no object field; only DapperQuery does, and you set it. Bind the
body as Query and upgrade with DapperQueryBuilder.FromBase(...).
If you genuinely need a client-selected dataset, map an allow-listed key to a name in your own code — never pass the client's string through.
The five rule sets are independent by design. Deny it in all four places — Select, Where, Sort
and GroupBy — or, better, point the endpoint at a view that does not contain the column at all.
Yes, on both axes. Values are always DbParameters and never concatenated. Column names are looked up
against the target's real columns and dropped if absent, then quoted by the dialect. Details:
Security.
SELECT on the objects you point it at. Nothing is deployed, migrated or created — that changed in
2.0.
Two for a flat query (a count and a page), three for a grouped one, one for a stored procedure. Plus a one-off schema probe the first time an object is seen, ever.
Cached per provider | schema | name | type for the lifetime of the process, with no invalidation
API. Restart the application after changing a table's shape, or new columns will be dropped from
queries.
That is EF Core's, and it composes normally:
await db.Users.AsNoTracking().ToQueryResultAsync<User>(query);Projected queries are untracked regardless, because EF Core does not track instances constructed inside a projection.
EF Core:
db.Users.ApplyQuery(query).ToQueryString();Dapper — the compiler is public, so no database is needed:
new SqlQueryCompiler(new PostgreSqlDialect()).CompileRows(dapperQuery, whitelist).Text;Implement ISqlDialect — thirteen members — and register it. The caveat is that
DapperDatabaseProvider is a closed enum, so an engine outside the five has no member of its own
today. Options and trade-offs:
Extending QueryForge.
Yes. Register one with the same ProviderType and it replaces the built-in. The built-ins are
sealed, so implement ISqlDialect and delegate for what you are not changing.
Not out of the box, but the extension point exists — write a provider. The core exposes the shared semantics so yours agrees with the others, and the two shared test suites tell you when it does. See Extending QueryForge.
Yes, in-memory, with a value accessor:
rows.ToQueryResult(query, InMemoryAccessors.ForDictionary<Dictionary<string, object?>>());Note this switches off column-existence checking. See In-Memory Provider.
var accessor = InMemoryAccessors.WithColumnMap<User>(new Dictionary<string, string>
{
["name"] = nameof(User.FirstName)
});For the database providers, alias the columns in a view so the names the caller uses are the names the target exposes.
Any that send filter, sort, group and paging descriptors — DevExtreme, AG Grid, Kendo UI. The
hierarchy shape is deliberately key / count / items, which is what those grids expect. Mappers:
Recipes.
Yes. Nothing depends on the web stack. The Dapper provider takes IServiceCollection for
registration, and that package is a dependency of the provider anyway — but you can also skip DI
entirely and use connection.QueryForgeAsync<T>(query).
Validation and query construction are pure and need nothing. For semantics, run the same query through the in-memory provider against seed data — it implements the same rules, so the test is meaningful about the database. See Testing.
The registry, executors, schema caches, compilers and dialects are all safe for concurrent use.
IDbConnection is not — that is ADO.NET's rule. Do not share one connection across concurrent calls.
https://github.com/PepperX-Dev/QueryForge/issues. For a query returning the wrong rows, include the
Query as JSON, the provider and engine, and the SQL if you can get it.
For a security issue, open a security advisory rather than a public issue.
MIT, for all four packages.
QueryForge · part of the PepperX Ecosystem · MIT licensed · packages 2.0.0, .NET 10
Foundations
- Getting Started
- Architecture
- Query Model
- Query Semantics
- Results and Metadata
- JSON Contract
- Fluent Builders
Behaviour
Providers
- Dapper Provider
- Dapper: Generated SQL
- Dapper: Dialects
- EF Core Provider
- EF Core: Joins & Includes
- In-Memory Provider
Practice
Reference