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.

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.


QueryCriteria

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.

ConditionGroup

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.

Condition

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.


QueryPaging

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.

SortDescriptor and GroupByDescriptor

public record SortDescriptor(string ColumnName, SortOrder SortOrder = SortOrder.Ascending)
    : IColumnDescriptor;

public record GroupByDescriptor(string ColumnName, SortOrder SortOrder = 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:

public interface IColumnDescriptor
{
    string ColumnName { get; }
}

Enumerations

Logic

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.

SortOrder

public enum SortOrder { Ascending, Descending }
Member JSON value
Ascending 0
Descending 1

ConditionOperator

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.

QueryResultType

public enum QueryResultType { Flat, Grouped }

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

QueryValidationMode

public enum QueryValidationMode { SilentStrip, ThrowException }

See Validation.


Result types

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 extension of the model

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.

Clone this wiki locally