Skip to content

Query Model

AmirHosseinMp02 edited this page Aug 1, 2026 · 1 revision

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.


Query

The root object. Deliberately a mutable class, not a record, because SilentStrip validation rewrites it in place.

publicclassQuery{publicQueryCriteriaCriteria{get;set;}=new();publicQueryPagingPaging{get;set;}=new();publicIReadOnlyList<string>SelectColumns{get;set;}=Array.Empty<string>();publicIReadOnlyList<SortDescriptor>SortColumns{get;set;}=Array.Empty<SortDescriptor>();publicIReadOnlyList<GroupByDescriptor>GroupByColumns{get;set;}=Array.Empty<GroupByDescriptor>();}
PropertyMeaningDefault
CriteriaThe filter tree.empty — matches everything
PagingWhich window of the result to return.Size = 12, Number = 1
SelectColumnsWhich columns to include. Empty means all.empty
SortColumnsOrdering, applied in list order.empty
GroupByColumnsGrouping 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.


QueryCriteria

A record holding groups of conditions and the operator that joins those groups.

publicrecordQueryCriteria{publicIReadOnlyList<ConditionGroup>Groups{get;init;}publicLogicLogic{get;init;}publicQueryCriteria(IReadOnlyList<ConditionGroup>?groups=null,Logiclogic=Logic.And);}
PropertyMeaning
GroupsThe condition groups. Guaranteed non-null; the constructor substitutes an empty array.
LogicHow 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.

ConditionGroup

publicrecordConditionGroup{publicIReadOnlyList<Condition>Conditions{get;init;}publicLogicLogic{get;init;}publicConditionGroup(IReadOnlyList<Condition>?conditions=null,Logiclogic=Logic.And);}
PropertyMeaning
ConditionsThe conditions in this group. Guaranteed non-null.
LogicHow 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.

Condition

publicrecordCondition(stringColumnName,ConditionOperatorOperator,object?Value=null,object?ValueTo=null);
PropertyMeaning
ColumnNameThe column or property to filter on. Matched case-insensitively.
OperatorWhich comparison to apply.
ValueThe value to compare against. Typed as object? so a JSON body can carry anything.
ValueToThe 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.


QueryPaging

publicrecordQueryPaging(intSize=12,intNumber=1);
PropertyMeaning
SizeRows per page — or, for a grouped query, outermost groups per page.
Number1-based page number.

Both are normalized identically by every provider before use:

size=Size>0?Size:12;// non-positive falls back to the defaultnumber=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.

SortDescriptor and GroupByDescriptor

publicrecordSortDescriptor(stringColumnName,SortOrderSortOrder=SortOrder.Ascending):IColumnDescriptor;publicrecordGroupByDescriptor(stringColumnName,SortOrderSortOrder=SortOrder.Ascending):IColumnDescriptor;

Structurally identical, semantically different:

  • SortColumns order the rows, applied in list order: the first is the primary key, the second breaks ties, and so on.
  • GroupByColumns define nesting levels, outermost first. Each level's SortOrder orders that level's keys. Paging applies to the first level only.

Both implement IColumnDescriptor, which is what lets the validation engine apply column rules generically:

publicinterfaceIColumnDescriptor{stringColumnName{get;}}

Enumerations

Logic

publicenumLogic{And,Or,AndNot,OrNot}
MemberJSON valueJoins withNegates
And0ANDno
Or1ORno
AndNot2ANDyes
OrNot3ORyes

Negation is meaningful only on a ConditionGroup. At the QueryCriteria level the Not suffix is ignored and only the AND/OR part is used.

SortOrder

publicenumSortOrder{Ascending,Descending}
MemberJSON value
Ascending0
Descending1

ConditionOperator

publicenumConditionOperator{Equals,NotEquals,Contains,NotContains,StartsWith,EndsWith,LessThan,GreaterThan,LessThanOrEqualTo,GreaterThanOrEqualTo,Between}
MemberJSON valueNeeds ValueNeeds ValueToNotes
Equals0nonoa null Value means IS NULL
NotEquals1nonoa null Value means IS NOT NULL
Contains2yesnotext; wildcards in the value are escaped
NotContains3yesnotext
StartsWith4yesnotext
EndsWith5yesnotext
LessThan6yesno
GreaterThan7yesno
LessThanOrEqualTo8yesno
GreaterThanOrEqualTo9yesno
Between10yesyesinclusive 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.

QueryResultType

publicenumQueryResultType{Flat,Grouped}

Reported on QueryResult<T>.Meta.Type, and tells you which of Models / Groups is populated.

QueryValidationMode

publicenumQueryValidationMode{SilentStrip,ThrowException}

See Validation.


Result types

Described in full under Results and Metadata; summarized here for completeness.

publicrecordQueryResult<TModel>{publicQueryResultMetaMeta{get;init;}publicIReadOnlyList<TModel>Models{get;init;}// when Type == FlatpublicIReadOnlyList<HierarchyNode<TModel>>Groups{get;init;}// when Type == Grouped}publicrecordQueryResultMeta(QueryResultMetaTotalTotal,QueryResultTypeType);publicrecordQueryResultMetaTotal(intRows,intPages);publicrecordHierarchyNode<TModel>(object?Key,intCount,IReadOnlyList<HierarchyNode<TModel>>?SubGroups,IReadOnlyList<TModel>?Items);

The Dapper extension of the model

The Dapper provider adds one thing — the target object — by subclassing Query.

namespacePepperX.QueryForge.Dapper;publicclassDapperQuery:PepperX.QueryForge.Query{publicDapperQueryObject?Object{get;set;}}publicrecordDapperQueryObject(stringName,stringSchema="",DapperObjectTypeType=DapperObjectType.Auto,IReadOnlyDictionary<string,object?>?Parameters=null);publicenumDapperObjectType{Auto=0,Table=1,View=2,TVF=3,SP=4}
FieldMeaning
NameTable, view, function or procedure name. Required.
SchemaEmpty means "use the dialect's default": dbo on SQL Server, public on PostgreSQL, none on MySQL, Oracle or SQLite.
TypeAuto, Table and View are all handled as "select from it". TVF and SP are invoked differently and must be declared.
ParametersArguments 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.

Clone this wiki locally