-
Notifications
You must be signed in to change notification settings - Fork 0
Query Model
Every type described here lives in the PepperX.QueryForge namespace in the core package. They
are plain, serializable, provider-free. This page is the structural reference — what the fields are.
Query Semantics is the behavioural reference — what they mean when evaluated.
The root object. Deliberately a mutable class, not a record, because SilentStrip validation
rewrites it in place.
public class Query
{
public QueryCriteria Criteria { get; set; } = new();
public QueryPaging Paging { get; set; } = new();
public IReadOnlyList<string> SelectColumns { get; set; } = Array.Empty<string>();
public IReadOnlyList<SortDescriptor> SortColumns { get; set; } = Array.Empty<SortDescriptor>();
public IReadOnlyList<GroupByDescriptor> GroupByColumns { get; set; } = Array.Empty<GroupByDescriptor>();
}| Property | Meaning | Default |
|---|---|---|
Criteria |
The filter tree. | empty — matches everything |
Paging |
Which window of the result to return. |
Size = 12, Number = 1
|
SelectColumns |
Which columns to include. Empty means all. | empty |
SortColumns |
Ordering, applied in list order. | empty |
GroupByColumns |
Grouping levels, outermost first. Non-empty switches the result to a hierarchy. | empty |
Every collection is initialized to empty and never null. A provider can enumerate any of them without a null check, and a client can omit any of them from a JSON body.
A Query has no notion of a table, entity, or collection. The target is supplied by the
provider — DapperQuery.Object for Dapper, the IQueryable<T> for EF Core, the IEnumerable<T> for
In-Memory. This is a security property, not an oversight: a client posting a Query cannot choose
what it runs against. See Security.
A record holding groups of conditions and the operator that joins those groups.
public record QueryCriteria
{
public IReadOnlyList<ConditionGroup> Groups { get; init; }
public Logic Logic { get; init; }
public QueryCriteria(IReadOnlyList<ConditionGroup>? groups = null, Logic logic = Logic.And);
}| Property | Meaning |
|---|---|
Groups |
The condition groups. Guaranteed non-null; the constructor substitutes an empty array. |
Logic |
How the groups are joined to each other. Only the AND/OR part is used here — a NOT suffix at this level is ignored. |
An empty Groups means no filter at all, which matches everything.
public record ConditionGroup
{
public IReadOnlyList<Condition> Conditions { get; init; }
public Logic Logic { get; init; }
public ConditionGroup(IReadOnlyList<Condition>? conditions = null, Logic logic = Logic.And);
}| Property | Meaning |
|---|---|
Conditions |
The conditions in this group. Guaranteed non-null. |
Logic |
How the conditions inside this group are joined, and whether the whole group is negated. AndNot and OrNot negate. |
A group whose conditions are all unusable contributes nothing and is skipped entirely — it does not become an always-false clause. See Query Semantics.
public record Condition(
string ColumnName,
ConditionOperator Operator,
object? Value = null,
object? ValueTo = null);| Property | Meaning |
|---|---|
ColumnName |
The column or property to filter on. Matched case-insensitively. |
Operator |
Which comparison to apply. |
Value |
The value to compare against. Typed as object? so a JSON body can carry anything. |
ValueTo |
The upper bound. Used only by Between, ignored by every other operator. |
Value and ValueTo may arrive as System.Text.Json.JsonElement when the query was model-bound
from an HTTP request. Providers never have to think about that — ConditionSemantics.Unwrap converts
it before use. See JSON Contract.
public record QueryPaging(int Size = 12, int Number = 1);| Property | Meaning |
|---|---|
Size |
Rows per page — or, for a grouped query, outermost groups per page. |
Number |
1-based page number. |
Both are normalized identically by every provider before use:
size = Size > 0 ? Size : 12; // non-positive falls back to the default
number = Number > 0 ? Number : 1;
offset = (number - 1) * size;A page past the end is not an error — it returns an empty page with the true totals still reported, which is what a data grid needs in order to correct itself.
public record SortDescriptor(string ColumnName, SortOrder SortOrder = SortOrder.Ascending)
: IColumnDescriptor;
public record GroupByDescriptor(string ColumnName, SortOrder SortOrder = SortOrder.Ascending)
: IColumnDescriptor;Structurally identical, semantically different:
-
SortColumnsorder the rows, applied in list order: the first is the primary key, the second breaks ties, and so on. -
GroupByColumnsdefine nesting levels, outermost first. Each level'sSortOrderorders that level's keys.Pagingapplies to the first level only.
Both implement IColumnDescriptor, which is what lets the validation engine apply column rules
generically:
public interface IColumnDescriptor
{
string ColumnName { get; }
}public enum Logic { And, Or, AndNot, OrNot }| Member | JSON value | Joins with | Negates |
|---|---|---|---|
And |
0 |
AND | no |
Or |
1 |
OR | no |
AndNot |
2 |
AND | yes |
OrNot |
3 |
OR | yes |
Negation is meaningful only on a ConditionGroup. At the QueryCriteria level the Not suffix
is ignored and only the AND/OR part is used.
public enum SortOrder { Ascending, Descending }| Member | JSON value |
|---|---|
Ascending |
0 |
Descending |
1 |
public enum ConditionOperator
{
Equals, NotEquals, Contains, NotContains,
StartsWith, EndsWith, LessThan, GreaterThan,
LessThanOrEqualTo, GreaterThanOrEqualTo, Between
}| Member | JSON value | Needs Value
|
Needs ValueTo
|
Notes |
|---|---|---|---|---|
Equals |
0 |
no | no | a null Value means IS NULL
|
NotEquals |
1 |
no | no | a null Value means IS NOT NULL
|
Contains |
2 |
yes | no | text; wildcards in the value are escaped |
NotContains |
3 |
yes | no | text |
StartsWith |
4 |
yes | no | text |
EndsWith |
5 |
yes | no | text |
LessThan |
6 |
yes | no | |
GreaterThan |
7 |
yes | no | |
LessThanOrEqualTo |
8 |
yes | no | |
GreaterThanOrEqualTo |
9 |
yes | no | |
Between |
10 |
yes | yes | inclusive at both ends |
The JSON values are the C# enum's declaration order. They are part of the wire contract — do not reorder them. Full semantics for each are in Query Semantics.
public enum QueryResultType { Flat, Grouped }Reported on QueryResult<T>.Meta.Type, and tells you which of Models / Groups is populated.
public enum QueryValidationMode { SilentStrip, ThrowException }See Validation.
Described in full under Results and Metadata; summarized here for completeness.
public record QueryResult<TModel>
{
public QueryResultMeta Meta { get; init; }
public IReadOnlyList<TModel> Models { get; init; } // when Type == Flat
public IReadOnlyList<HierarchyNode<TModel>> Groups { get; init; } // when Type == Grouped
}
public record QueryResultMeta(QueryResultMetaTotal Total, QueryResultType Type);
public record QueryResultMetaTotal(int Rows, int Pages);
public record HierarchyNode<TModel>(
object? Key,
int Count,
IReadOnlyList<HierarchyNode<TModel>>? SubGroups,
IReadOnlyList<TModel>? Items);The Dapper provider adds one thing — the target object — by subclassing Query.
namespace PepperX.QueryForge.Dapper;
public class DapperQuery : PepperX.QueryForge.Query
{
public DapperQueryObject? Object { get; set; }
}
public record DapperQueryObject(
string Name,
string Schema = "",
DapperObjectType Type = DapperObjectType.Auto,
IReadOnlyDictionary<string, object?>? Parameters = null);
public enum DapperObjectType { Auto = 0, Table = 1, View = 2, TVF = 3, SP = 4 }| Field | Meaning |
|---|---|
Name |
Table, view, function or procedure name. Required. |
Schema |
Empty means "use the dialect's default": dbo on SQL Server, public on PostgreSQL, none on MySQL, Oracle or SQLite. |
Type |
Auto, Table and View are all handled as "select from it". TVF and SP are invoked differently and must be declared. |
Parameters |
Arguments for a function or procedure. TVF arguments are positional, passed in the order of this dictionary. Procedure arguments are named where the engine supports it. |
DapperQuery is deliberately a separate type from Query. Bind the client's body as a Query,
then upgrade it server-side with DapperQueryBuilder.FromBase(clientQuery) — the target is something
only your code can set. See Fluent Builders and Security.
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