UniversalQueryBuilder.Endpoints 10.0.13-beta

This is a prerelease version of UniversalQueryBuilder.Endpoints.
dotnet add package UniversalQueryBuilder.Endpoints --version 10.0.13-beta
                    
NuGet\Install-Package UniversalQueryBuilder.Endpoints -Version 10.0.13-beta
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="UniversalQueryBuilder.Endpoints" Version="10.0.13-beta" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="UniversalQueryBuilder.Endpoints" Version="10.0.13-beta" />
                    
Directory.Packages.props
<PackageReference Include="UniversalQueryBuilder.Endpoints" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add UniversalQueryBuilder.Endpoints --version 10.0.13-beta
                    
#r "nuget: UniversalQueryBuilder.Endpoints, 10.0.13-beta"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package UniversalQueryBuilder.Endpoints@10.0.13-beta
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=UniversalQueryBuilder.Endpoints&version=10.0.13-beta&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=UniversalQueryBuilder.Endpoints&version=10.0.13-beta&prerelease
                    
Install as a Cake Tool

QueryBuilder.Endpoints

Minimal API endpoints for Universal Query Builder. Provides ready-to-use HTTP endpoints for query execution, schema introspection, and validation.

Installation

dotnet add package UniversalQueryBuilder.Endpoints

Registration

// 1. Register services
builder.Services.AddQueryBuilder();  // Core QueryBuilder services
builder.Services.AddQueryBuilderEndpoints(options =>
{
    options.RoutePrefix = "/api/query";              // Default: "/api/query"
    options.RegistryRoutePrefix = "/api/registry";   // Default: "/api/registry"
    options.DefaultPageSize = 50;                    // Default: 50
    options.MaxPageSize = 1000;                      // Default: 1000
    options.DefaultBulkBatchSize = 1000;             // Default: 1000
    options.MaxBulkKeysPerRequest = 50000;           // Default: 50000
    options.MaxTotalResultRows = 100000;             // Default: 100000
    options.BulkOperationTimeoutSeconds = 120;       // Default: 120
    options.EnableValidation = true;                 // Default: true
    options.EnableSchemaIntrospection = true;        // Default: true
    options.IncludeDebugInfo = false;                // Default: false
});

// 2. Map endpoints
app.MapQueryBuilderEndpoints();

Endpoints

All query and validation endpoints are rooted at RoutePrefix (default /api/query). Registry endpoints are rooted at RegistryRoutePrefix (default /api/registry).

Query Execution

POST {RoutePrefix}/{sourceName} - Execute a query against a data source

Request Properties
Property Type Description
query string Shorthand filter syntax (e.g., "status:active AND age:>18")
where FilterDefinition Structured JSON filter (see below)
select object Hierarchical field selection
groupBy string[] Explicit grouping fields for aggregation
limit int Maximum results (clamped between 1 and MaxPageSize)
offset int Pagination offset (minimum 0)
includeTotalCount bool Include total count (default: true)
includeDebug bool Include debug block when server IncludeDebugInfo is enabled (default: false)
orderBy OrderingDefinition[] Sort specification for main result set (see Ordering)
searchGroup string Restrict bare-term search to a named field group (see Search Groups)

When limit is not specified, DefaultPageSize is used. Values exceeding MaxPageSize are clamped down; values below 1 are clamped up. offset is floored at 0.

Filter Combination

When both where (structured) and query (shorthand) are provided, they combine with AND logic:

  • where acts as the baseline filter (like a page-level filter)
  • query acts as a refinement (like a user search filter)
  • Effective filter: where AND parsedQuery
Example: Shorthand Only
{
  "query": "isActive:true AND age:>18",
  "select": { "id": true, "firstName": true },
  "limit": 50
}
Example: Structured Filter Only
{
  "where": {
    "logicalOperator": "and",
    "expressions": [
      { "field": "departmentId", "operator": "in", "values": [1, 2, 3] },
      { "field": "createdAt", "operator": "gte", "value": "2024-01-01" }
    ]
  },
  "select": { "id": true, "firstName": true },
  "limit": 50
}
Example: Combined (Baseline + Refinement)
{
  "where": {
    "field": "departmentId",
    "operator": "in",
    "values": [1, 2, 3]
  },
  "query": "status:pending",
  "select": { "id": true, "firstName": true, "status": true },
  "limit": 50
}

Effective filter: departmentId IN (1,2,3) AND status = 'pending'

@ Function Resolution

Filter values starting with @ (e.g., @this_week, @today, @last_7_days) are resolved to concrete values before expression building. Date macros resolve to Between operations with computed date ranges. This applies to both structured (where) and shorthand (query) filters.

{
  "where": {
    "field": "startDate",
    "operator": "eq",
    "value": "@this_week"
  }
}

The @this_week value resolves at execution time to a Between filter spanning the current week's start and end dates.

Ordering

The orderBy property controls the sort order of the main result set. When provided, ordering is applied before pagination.

{
  "query": "department:engineering",
  "orderBy": [
    { "field": "lastName", "direction": "asc" },
    { "field": "firstName", "direction": "asc" }
  ],
  "limit": 50
}

Each ordering item specifies:

Property Type Description
field string Column name or dot-notation navigation path (must be an exposed field in the data source schema)
direction string "asc" or "desc" (default: "asc")
nulls string "first" or "last" (optional)

Navigation property ordering: The field property supports dot-notation paths for scalar (reference) navigation properties. For example, "address.city" or "company.headquartersAddress.city". Direct field-based ordering by collection navigation properties (one-to-many) is not supported because it is semantically ambiguous — use an aggregate function instead.

{
  "orderBy": [
    { "field": "address.city", "direction": "asc" },
    { "field": "lastName", "direction": "asc" }
  ]
}

Aggregate function ordering: Use the function property to order by an aggregate over a collection navigation. Supported functions: count, sum, avg, min, max.

{
  "orderBy": [
    {
      "function": { "name": "min", "arguments": [{ "field": "onboardings.startDate" }] },
      "direction": "asc"
    },
    {
      "function": { "name": "count", "arguments": [{ "field": "orders" }] },
      "direction": "desc"
    }
  ]
}

Each ordering item specifies exactly one of field or function (not both). The function.arguments[0].field must reference a path through a collection navigation property in the data source schema.

Grouped (GROUP BY) ordering: In a grouped query, ordering is validated against the grouped projection, not the source columns — the result rows are keyed by the select aliases. Order by a select alias that projects a scalar value: the GROUP BY key or a top-level count alias, using the bare field form.

{
  "groupBy": ["lanId"],
  "select": { "lanId": true, "count": true },
  "orderBy": [{ "field": "count", "direction": "desc" }],
  "limit": 50
}

This returns the top groups by count — ordering is applied to the groups before the page limit. Three forms are rejected with a 400: a source column that the projection does not include (it is not a result key); a per-column aggregate (e.g. { "salary": { "avg": true } } projects a nested object, not a sortable scalar); and the aggregate-function ordering form (use the aggregate's select alias instead — the function form is for collection-navigation ordering on non-grouped queries).

Deterministic pagination: When orderBy is provided, the data source's primary key fields are automatically appended as tiebreakers if not already present. This ensures stable page boundaries when sorting by non-unique fields.

Default ordering: When orderBy is omitted, the data source's configured default ordering is used (typically the primary key).

Validation: For a non-grouped query, all fields must exist in the data source schema; for a grouped query, they must be sortable projection aliases (see Grouped ordering above). Invalid fields return a 400 response with the field name and the available/sortable fields listed.

Search Groups

The searchGroup property narrows bare-term expansion to a named subset of searchable fields. When set, only the group's fields participate in search, regardless of the group's declared exclusivity mode (consumer selection is always exclusive).

The group name must match a group registered via .SearchGroup() in the data source configuration. If the name does not match any registered group, the request returns a 400 error with the available group names.

{
  "query": "john",
  "searchGroup": "name",
  "limit": 50
}

Without searchGroup, bare terms expand across all searchable fields (subject to any auto-activating exclusive groups). With searchGroup, expansion is restricted to only that group's fields.

Structured Filter Operators
Category Operators
Comparison eq, ne, gt, gte, lt, lte
String like, startsWith, endsWith, contains
Collection in, notIn
Range between, notBetween
Null isNull, isNotNull
Logical and, or, not
Response
{
  "items": [...],
  "totalCount": 150,
  "page": 1,
  "pageSize": 50,
  "orderBy": [
    { "field": "lastName", "direction": "asc" },
    { "field": "firstName", "direction": "asc" },
    { "field": "id", "direction": "asc" }
  ],
  "debug": {
    "parsedQuery": { ... },
    "efDebugSql": "SELECT ...",
    "warnings": []
  }
}

totalCount is nullable — omitted from JSON when null. It is null when includeTotalCount=false or when the execution strategy does not compute a count.

orderBy shows the effective ordering applied, including any tiebreaker fields appended for deterministic pagination. Omitted when no ordering was applied.

debug is optional and only returned when both includeDebug=true and server option IncludeDebugInfo=true.

Bulk Lookup By Key

POST {RoutePrefix}/{sourceName}/bulk/by-key - Lookup many records by a key field in one request

Request body:

{
  "keys": ["SN-001", "SN-002", "SN-003"],
  "keyField": "SerialNumber",
  "select": { "serialNumber": true, "status": true },
  "orderBy": [{ "field": "Status", "direction": "asc" }],
  "includeDebug": false
}
Property Type Description
keys string[] Required. Input keys (deduplicated server-side after type conversion). For composite keys, use tilde-delimited format (e.g., ["SAP~42", "FOO~1"])
keyField string Optional. Canonical exposed top-level scalar field; defaults to primary key. When keyField is explicitly provided on a composite-key source, a single-column IN-based lookup is used instead of composite key matching
select object Optional projection selection
orderBy OrderingDefinition[] Optional ordering applied to rows within each key's results array
includeDebug bool Include debug payload when server IncludeDebugInfo is enabled
Single Key Example
{
  "keys": ["SN-001", "SN-002", "SN-003"],
  "keyField": "SerialNumber",
  "select": { "serialNumber": true, "status": true }
}
Composite Key Example

When keyField is omitted and the source has a composite primary key, keys use the same tilde-delimited format as get-by-ID:

{
  "keys": ["SAP~42", "FOO~1"],
  "select": { "sourceName": true, "id": true, "status": true }
}

Each key string is deserialized into typed parts matching the composite key definition. The response includes a keyFields array listing the composite key property names.

Buffered JSON response (single key):

{
  "sourceName": "devices",
  "keyField": "SerialNumber",
  "keyFields": ["SerialNumber"],
  "items": [
    { "key": "SN-001", "found": true, "results": [{ "serialNumber": "SN-001", "status": "Active" }] }
  ],
  "summary": {
    "requestedCount": 3,
    "distinctCount": 2,
    "foundCount": 1,
    "missingCount": 1,
    "matchedRecordCount": 1,
    "batchCount": 1,
    "effectiveBatchSize": 1000
  },
  "executionTimeMs": 18,
  "executionStrategy": "EntityFramework"
}

Buffered JSON response (composite key):

{
  "sourceName": "orderItems",
  "keyField": "SourceName,Id",
  "keyFields": ["SourceName", "Id"],
  "items": [
    { "key": "SAP~42", "found": true, "results": [{ "sourceName": "SAP", "id": 42, "status": "Active" }] }
  ],
  "summary": { "..." : "..." },
  "executionTimeMs": 22,
  "executionStrategy": "EntityFramework"
}

The keyFields property is always present — single-element for simple keys, multi-element for composite keys. keyField is derived as string.Join(",", keyFields).

Batch sizing: For composite keys, batch sizes are dynamically calculated to stay within SQL Server parameter limits (2000 parameters per batch). The effective batch size is min(DefaultBulkBatchSize, max(1, 2000 / keyPartCount)).

For streaming, send Accept: application/x-ndjson (or application/jsonl). The endpoint emits NDJSON events:

  • item (one per distinct key)
  • progress (after each completed batch)
  • debug (when enabled)
  • summary (final aggregate event)
  • error (runtime failure after stream start)

Notes:

  • Duplicate input keys are deduplicated server-side.
  • Response ordering is not guaranteed; correlate by key.
  • Vary: Accept is returned because response format depends on Accept.
  • String keys correlate and deduplicate case-insensitively to align with SQL Server's default case-insensitive collation (SQL_Latin1_General_CP1_CI_AS). Requesting "zisk" matches a row stored as "ZISK", and submitting ["zisk", "ZISK"] produces a single distinct key. Numeric, Guid, and other non-string key types use their default equality.

Get By ID

POST {RoutePrefix}/{sourceName}/{id} - Retrieve a single record by primary key

Request body (optional):

{
  "select": {
    "id": true,
    "username": true,
    "department": { "select": { "name": true } }
  },
  "includeDebug": true
}
Property Type Description
select object Hierarchical field selection (optional)
includeDebug bool Include debug block when server IncludeDebugInfo is enabled (default: false)
Single Key
POST /api/query/users/42
Composite Key

Composite keys use tilde-delimited format in the URL path. Each key part is separated by a single tilde (~). A literal tilde in a key value is escaped as ~~.

POST /api/query/orderItems/SAP~42

This resolves to SourceName = "SAP" and Id = 42 for a composite key with parts (SourceName, Id).

Escaping example: a key value containing a literal tilde (e.g., A~B and 7) is encoded as A~~B~7.

Example with projection:
POST /api/query/users/42
Content-Type: application/json

{
  "select": {
    "id": true,
    "email": true
  }
}

Response (200):

{
  "result": {
    "id": 42,
    "username": "jdoe",
    "email": "jdoe@example.com"
  },
  "sourceName": "users",
  "executionTimeMs": 12,
  "executionStrategy": "EntityFramework",
  "debug": {
    "parsedQuery": { ... },
    "efDebugSql": "SELECT ...",
    "warnings": []
  }
}

debug is optional and only returned when both includeDebug=true and server option IncludeDebugInfo=true.

Response (404 - Record not found):

{
  "type": "https://www.rfc-editor.org/rfc/rfc9457",
  "title": "Record not found",
  "status": 404,
  "detail": "Record with ID '999' was not found.",
  "traceId": "0HN7Q4E8M63KJ:00000001"
}

Response (400 - No primary key configured):

{
  "type": "https://www.rfc-editor.org/rfc/rfc9457",
  "title": "Primary key not configured",
  "status": 400,
  "detail": "This data source does not support record lookup by ID.",
  "traceId": "0HN7Q4E8M63KJ:00000001"
}

Requirements:

  • Data source must have a primary key configured (via [Key] attribute or WithPrimaryKey())
  • Supports both single and composite primary keys
  • The ID is automatically converted to the correct CLR type (int, Guid, string, etc.)
  • For composite keys, each tilde-delimited segment is independently converted to its corresponding part's CLR type
  • Projection semantics are identical to POST {RoutePrefix}/{sourceName} (query-many) for the same select payload
  • When select is omitted, endpoints build a schema-default recursive selection using canonical schema field casing

ID coercion behavior:

  • Conversion is delegated to TypeCoercer.TryCoerce from the TypeCoercion NuGet package.
  • Coercion is strict (no silent fallback conversions).
  • For single keys, invalid ID values raise QueryBuilder.Core.Exceptions.QueryValidationException, producing a 400 ProblemDetails response with the coercion error message keyed to "id".
  • For composite keys, segment count mismatches or type conversion failures produce a 400 ProblemDetails response identifying the expected key parts and the failing segment.

Validation

POST {RoutePrefix}/validate - Validate shorthand query syntax without executing

Both query and sourceName are required. The source name determines which schema to validate field names against.

Request body:

{
  "query": "isActive:true AND age:>18",
  "sourceName": "users"
}

Response (success):

{
  "valid": true,
  "query": "isActive:true AND age:>18",
  "parsedQuery": { ... },
  "executionTimeMs": 2
}

Response (failure):

{
  "type": "https://www.rfc-editor.org/rfc/rfc9457",
  "title": "Query validation failed",
  "status": 400,
  "errors": {
    "query": ["Syntax error at position 15"]
  },
  "traceId": "0HN7Q4E8M63KJ:00000001"
}

POST {RoutePrefix}/{sourceName}/validate - Validate a full query request against the data source schema

Validates structured filters, shorthand queries, ordering, select projections, and groupBy fields without executing. Returns all validation errors at once — does not fail on the first error.

Request body (same shape as the execution endpoint):

{
  "query": "isActive:true",
  "where": { "field": "status", "operator": "in", "values": ["Active"] },
  "orderBy": [{ "field": "lastName", "direction": "asc" }],
  "select": { "id": true, "firstName": true }
}

Response (valid):

{
  "valid": true,
  "query": "isActive:true",
  "parsedQuery": { ... },
  "executionTimeMs": 3
}

Response (invalid — returns all errors):

{
  "valid": false,
  "query": "isActive:true",
  "errors": [
    {
      "code": "FieldNotFound",
      "fieldName": "orderBy[0].field",
      "title": "Invalid ordering specification",
      "detail": "Field 'lastName' is not a sortable field. Available fields: ..."
    }
  ],
  "executionTimeMs": 2
}

Schema Registry Introspection

Registry endpoints use RegistryRoutePrefix (default /api/registry), separate from the query prefix.

GET {RegistryRoutePrefix}/sources - List all registered data sources

GET {RegistryRoutePrefix}/sources/{sourceName} - Get specific data source

GET {RegistryRoutePrefix}/sources/{sourceName}/columns - Get column metadata (hierarchical tree structure)

GET {RegistryRoutePrefix}/sources/{sourceName}/columns/{columnName} - Get specific column metadata

Column metadata fields
Field Type Description
columnName string Column segment name (not full dotted path)
displayName string? User-friendly display name
description string? Optional description
dataType string? CLR type name ("int", "string", "OrderStatus", "Collection<OrderItem>")
isNullable bool Whether the column allows NULL values
isCollection bool Whether the column is a collection/array type
isEnum bool Whether the column's CLR type is an enum (including nullable enums). Used by UI builders to render dropdown/checkbox list editors
isRecommended bool Whether this column is a suggested/recommended filter for UI builders
collectionElementType string? Element type name for navigation collections
maxLength int? Maximum length for character or binary columns
numericPrecision int? Numeric precision for decimal/numeric columns
numericScale int? Numeric scale for decimal/numeric columns
isPrimaryKey bool Whether this column is part of the primary key
aliases string[] Field aliases for shorthand query support (always non-null; empty when none)
children List<ColumnMetadataDto> Nested child columns for navigation properties
isRestricted bool Whether the current user lacks permission to query this column. When true, the column is visible in discovery but aliases, metadata, and children are suppressed and querying it returns 403 with errorCode column_permission_denied. Always false unless an IColumnAuthorizationService is registered.
valueSource ColumnValueSourceKind Indicates how the column's value is produced. One of Property (direct CLR property access), JsonScalar (SQL Server JSON_VALUE extraction from a backing JSON column), or Coalesce (null-coalescing composition of one or more sources with an optional terminal literal fallback). Mirrors the closed ColumnAccessor variant set in the SchemaRegistry.

GET {RegistryRoutePrefix}/sources/{sourceName}/templates - Get configured template metadata for a data source

GET {RegistryRoutePrefix}/sources/{sourceName}/templates/{templateName} - Get metadata for a specific configured template

GET {RegistryRoutePrefix}/sources/{sourceName}/columns/{columnPath}/values - Get distinct values for a scalar column

Query parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | search | string? | null | Case-insensitive contains filter (string columns only, ignored for enums) | | limit | int | 50 | Maximum values to return (1-200) |

Response:

{
  "values": ["Active", "Cancelled", "Pending", "Shipped"],
  "hasMore": false
}

Enum columns return all member names via CLR reflection (no database hit). Non-enum columns execute a synthetic DISTINCT query through the strategy pipeline. Null values are excluded — use the column's isNullable metadata to inform the UI whether null is a possible value.

This endpoint uses query-level authorization (CanQueryAsync), not discover-level (CanDiscoverAsync), because it reads data values. Column-level authorization runs identically to query execution — a request for distinct values of a restricted column returns 403 with errorCode: column_permission_denied.

GET {RegistryRoutePrefix}/summary - Get registry overview with counts

Registry endpoints enforce source-level authorization via ISourceAuthorizationFilter. The behavior depends on the endpoint type:

  • List endpoints (/sources, /summary) — filter results, returning only sources where CanDiscoverAsync returns true. Non-discoverable sources are silently omitted. The /summary endpoint adjusts TotalDataSources to reflect the filtered count.
  • Single-source endpoints (/sources/{sourceName}, /sources/{sourceName}/columns, /sources/{sourceName}/columns/{columnName}, /sources/{sourceName}/templates, /sources/{sourceName}/templates/{templateName}) — return 404 if CanDiscoverAsync returns false for the requested source, preventing source enumeration.
  • Distinct values endpoint (/sources/{sourceName}/columns/{columnPath}/values) — uses CanQueryAsync (not CanDiscoverAsync) for query-level authorization. Returns 404 if unauthorized.

Example response (GET {RegistryRoutePrefix}/sources/users/templates):

[
  {
    "templateName": "searchUsers",
    "description": "Search users by role and age",
    "pattern": "role:{roleName} AND age:>={minAge}",
    "parameters": [
      {
        "name": "roleName",
        "description": "Role name",
        "expectedType": "string",
        "defaultValue": "Engineer",
        "isRequired": false,
        "position": 0,
        "validationRules": [
          {
            "kind": "regex",
            "ruleType": "RegexValidationRule",
            "pattern": "^[A-Za-z]+$"
          }
        ]
      },
      {
        "name": "minAge",
        "description": "Minimum age",
        "expectedType": "int",
        "defaultValue": null,
        "isRequired": true,
        "position": 1,
        "validationRules": [
          {
            "kind": "range",
            "ruleType": "RangeValidationRule",
            "minimum": 18,
            "maximum": 120
          },
          {
            "kind": "custom",
            "ruleType": "LambdaValidationRule"
          }
        ]
      }
    ]
  }
]

See Source Authorization Filtering.

Configuration Options

Option Type Default Description
RoutePrefix string /api/query Base path for query and validation endpoints
RegistryRoutePrefix string /api/registry Base path for registry introspection endpoints
EnableSchemaIntrospection bool true Enable registry endpoints
EnableValidation bool true Enable /validate endpoint
IncludeDebugInfo bool false Enables debug metadata for get-by-id responses, query responses, and bulk lookup responses when includeDebug=true
DefaultPageSize int 50 Default limit when not specified in request
MaxPageSize int 1000 Upper bound for limit; values exceeding this are clamped
DefaultBulkBatchSize int 1000 Server-side batch size for bulk key lookup IN queries (must be >= 1)
MaxBulkKeysPerRequest int 50000 Maximum number of input keys accepted by /bulk/by-key (must be >= 1)
MaxTotalResultRows int 100000 Maximum total matched rows returned across all bulk batches (must be >= 1)
BulkOperationTimeoutSeconds int 120 Maximum wall-clock time for an entire bulk lookup operation (must be >= 1)
QueryEndpointTag string Query OpenAPI tag for query endpoints
RegistryEndpointTag string Schema Registry OpenAPI tag for registry endpoints

The four bulk options (DefaultBulkBatchSize, MaxBulkKeysPerRequest, MaxTotalResultRows, BulkOperationTimeoutSeconds) validate at assignment time and throw ArgumentOutOfRangeException for values less than 1.

Debug Info

IncludeDebugInfo applies to optional debug metadata in get-by-id success responses, query success responses, and bulk lookup responses when includeDebug=true is requested. Enable in development only:

builder.Services.AddQueryBuilderEndpoints(options =>
{
    options.IncludeDebugInfo = builder.Environment.IsDevelopment();
});

Empty Parse Results

When the shorthand parser returns IsEmpty = true (e.g., UnmatchedBareTermBehavior.EmptyResult mode in QueryBuilder.Shorthand), the query execution service short-circuits with an empty PagedResult:

  • Zero items, TotalCount = 0, Page = 1
  • PageSize reflects the requested limit (or DefaultPageSize), clamped to [1, MaxPageSize]
  • No database query is executed

This enables live-search UIs where partial input naturally produces no matches without incurring a database round-trip.

Parse failure wrapping: When parsing fails, GetQueryOrThrow() throws a CoreException with ParseFailure error code, and the execution service wraps it as a validation error (400 response with the parse error message keyed to "query"). Other CoreException types propagate unmodified.

Error Responses

Status Condition
200 Query executed successfully (includes empty results from IsEmpty parse results)
400 Invalid query syntax, bulk key validation errors, no execution strategy, execution failure, no primary key configured, invalid ID format, invalid composite key format, bulk timeout, or exceeded MaxTotalResultRows
403 Source-level denial (user lacks permission to query the source, or a subquery references a source the user cannot query — errorCode: source_permission_denied); column-level denial (user lacks permission for a referenced column — errorCode: column_permission_denied)
404 Data source not found in schema registry, user cannot discover the source (intentionally indistinguishable from not-found), record not found by ID, or CoreException(ResourceNotFound) (e.g., a row-level filter references an unregistered subquery source)
500 Authorization configuration fault (e.g., source-auth filter bypassed, permission evaluator threw — diagnostic message preserved in body); CoreException from the row-filter injector (factory throw, cyclical config) with sourceName and other ex.Context entries surfaced as ProblemDetails extensions; unhandled exception

All endpoint/filter errors use RFC9457 problem details:

{
  "type": "https://www.rfc-editor.org/rfc/rfc9457",
  "title": "Data source not found",
  "status": 404,
  "detail": "Data source 'users' was not found.",
  "traceId": "0HN7Q4E8M63KJ:00000001"
}

Validation failures use RFC9457 validation problem details with keyed errors:

{
  "type": "https://www.rfc-editor.org/rfc/rfc9457",
  "title": "Query validation failed",
  "status": 400,
  "errors": {
    "query": ["Syntax error at position 5"]
  },
  "traceId": "0HN7Q4E8M63KJ:00000001"
}

Validation Pipeline

QueryRequestValidator.ValidateAsync is the single orchestrator for POST {RoutePrefix}/{sourceName} and POST {RoutePrefix}/{sourceName}/validate. It runs these steps in order:

  1. Validate the structured filter (request.Where) against the source schema, returning errors keyed to where.*.
  2. Parse shorthand (request.Query) and combine: effectiveFilter = request.Where ∧ shorthand. When shorthand short-circuits to IsEmpty, downstream steps that would do work for nothing are skipped.
  3. Inject row-level filter via IRowFilterInjector.InjectAsync(queryDef, ct) for the outer source and every reachable subquery that references a row-filtered source. The injector mutates queryDef.Where in place and resolves the outer source internally from queryDef.From. Runs before @-resolution so injected references (e.g., a filter that AND-composes OwnerId = @me) resolve in the same pass as the caller's WHERE. Skipped when the shorthand parser short-circuited to an empty result OR when shorthand parsing failed (a partial WHERE would surface a row-filter throw as 500, masking the underlying 400). CoreException from the injector (cyclical config, factory throw, unregistered subquery source) propagates to the endpoint filter, which maps it to 500/404 with ex.Context preserved as ProblemDetails extensions.
  4. Materialize per-request features via IFunctionExecutionFeatureCollector.CollectAsync(ct) (invoked unconditionally, once per request). The features feed @-resolution in step 9 — they are collected here so the row filter's injected references are covered without re-running providers.
  5. Validate and resolve ordering, materializing the default ordering when the caller supplied none. For a non-grouped query, order fields are validated against the source schema; for a grouped query (groupBy present), they are validated against the grouped projection's sortable output keys — the GROUP BY key and top-level count aliases classified by the shared GroupedSelectClassifier — so a grouped orderBy on count validates and the aggregate-function ordering form (silently ignored after GROUP BY) is rejected. groupBy/select are validated for their own correctness in steps 6–7; here they only scope what "sortable" means.
  6. Validate groupBy, ensuring at most one grouping field is supplied (multiple fields fail with InvalidQueryStructure) and that every grouping field is exposed. This runs before select so the grouped-select walk can rely on the group-by field resolving. A request with groupBy but no select is rejected with RequiredFieldMissing — grouped queries require an explicit projection.
  7. Validate select, dispatching on grouping state:
    • Grouped (groupBy present): the projection is checked against the grouped-select contract via GroupedSelectClassifier (the SSOT shared with the EF aggregation builder). The group key must be projected (by its canonical name or any alias) and every other entry must be an aggregate (count, or per-column sum/avg/average/min/max). Bare non-grouped columns, unsupported aggregates, excluded leaves, and a select that omits the group key entirely are rejected with coded errors. The grouped-select walk only runs for a single, resolved group-by field — a multi-field or unresolved groupBy reports its own error without piling on spurious shape errors.
    • Non-grouped: a stray top-level aggregate keyword that does not resolve to a real column is rejected with InvalidQueryStructure (not FieldNotFound); otherwise the projection routes through the nested-selection validator, ensuring every projected field is exposed and the caller can read it.
  8. Apply column authorization — walks every column reference (including those introduced by the row filter and those reached through subqueries) and enforces per-column permissions. Row-filter field references are checked here as defense in depth. Also materializes the auth-aware default selection when the caller supplied no explicit SELECT/GROUP BY.
  9. Reject @ in expression operands, then resolve @ references over the fully assembled graph. Resolution is deferred to here — after column authorization — so it sees the final graph (WHERE + injected row filter + ORDER BY + SELECT + GROUP BY + the auth-aware default selection); resolving at the WHERE-only stage would silently never resolve a projection-only or ordering-only @me. Resolution recurses into every filter-bearing clause (WHERE/HAVING, nested SelectionConfig.Where, ordering function filters, CASE-WHEN conditions, subqueries) via the shared QueryDefinitionWalker. Two guards run first: (a) an @ placed in an expression operand (CASE/COALESCE/CAST/arithmetic ExpressionDefinition.Value) or a function-argument value (FunctionArgument.Value) — slots the resolver never rewrites — is rejected with an InvalidFormat error rather than leaking the literal "@…" to the provider; (b) resolution is gated on errors.Count == 0, so a query already known invalid is never resolved into a partially-@-rewritten graph, and resolver-time failures (e.g. @me with no UserContextFeature) surface as a 400 where validation error rather than a 5xx. The operand reject is request (untrusted-input) validation only; trusted in-process callers that build a QueryDefinition directly are not guarded.

The single-field-groupBy and groupBy ⇒ select rules are endpoint-only guards; validation is strategy-blind by design. A direct QueryDefinition/EF caller that bypasses QueryRequestValidator is not subject to them — the EF strategy uses the first group-by field and the default-selection path.

BulkLookupService and DistinctValuesService build their own QueryDefinition instances and bypass the validator. They invoke IRowFilterInjector.InjectAsync themselves before strategy dispatch:

  • BulkLookupService invokes the injector once per request on a skeleton query (no WHERE, no SELECT) and AND-composes the resulting predicate onto each batch's caller-built WHERE. The composed row filter is also AND-composed into the representative WHERE that drives column authorization, so a row filter referencing a restricted column denies the request rather than silently bypassing column auth — matching the validator's inject-then-auth ordering. The skeleton trick keeps the injector's contract intact: any row filter that itself contains subqueries against other row-filtered sources is fully composed during the single invocation, so the per-batch work is just an AND.
  • DistinctValuesService also runs the injector once on a skeleton query, AND-composes the result into the auth query for column-auth validation, and AND-composes the same predicate into the synthetic DISTINCT query before strategy dispatch. The factory runs exactly once per request even when the enum-reflection path is taken.

Both services share the same IFunctionExecutionFeatureCollector, so @me and other contextual @ functions resolve consistently with the main /query pipeline.

QueryExceptionEndpointFilter is the single source of truth for exception → HTTP response mapping across all query builder endpoints. It maps CoreException(ResourceNotFound) to 404 and other CoreException instances to 500 (preserving ex.Context — notably sourceName from row-filter injector faults — as ProblemDetails extensions). See the Error Responses table for the full mapping.

Column-Level Authorization

In addition to source-level checks, a column-authorization pipeline runs after request validation. It walks every column reference produced by the request — including those reached through subqueries, nested Where, OrderBy, Select, GroupBy, and Having clauses — and enforces the schema's per-column policies (configured via RequireColumnPermission(c => c.PersonnelNumber, "read:pii") in the schema registry; see QueryBuilder.SchemaRegistry/README.md).

The default IColumnAuthorizationService registration is a permit-all OpenColumnAuthorizationService — it never denies and reports no restricted paths, so the column-auth pipeline is a no-op until UseMetadataSourceAuthorization(...) swaps in MetadataColumnAuthorizationService. Consumers therefore never need to null-check the dependency or special-case "auth disabled" code paths.

Default-selection auto-skip

When a request supplies no Select and no GroupBy, the validator builds the default projection from the source's columns and omits any column the user lacks access to. The resulting query succeeds (200 OK) and returns just the columns the caller is allowed to see.

When the request supplies an explicit Select referencing a restricted column, the request is rejected with 403 — the caller asked for something they cannot have, so the rejection is explicit.

Column denial response

A column-level denial returns HTTP 403 with this RFC 9457 problem details shape:

{
  "type": "https://www.rfc-editor.org/rfc/rfc9457",
  "title": "Column access denied",
  "status": 403,
  "detail": "Access to column 'PersonnelNumber' on source 'coworkers' requires permission 'read:pii'.",
  "errorCode": "column_permission_denied",
  "sourceName": "coworkers",
  "columnPath": "PersonnelNumber",
  "permission": "read:pii",
  "traceId": "0HN7Q4E8M63KJ:00000001"
}

Fields:

  • errorCode: "column_permission_denied" — discriminator for client-side handling
  • sourceName — the data source whose column triggered the denial. May differ from the outer From source when the denial originates inside a subquery
  • columnPath — canonical dotted path to the restricted column (e.g., "Address.Zip"). When an ancestor is restricted, this is the ancestor path, not a deeper descendant
  • permission — the permission key the user lacked

Subquery source denial

A subquery referencing a foreign data source the caller cannot access at the source level fails with 403 and a different errorCode:

{
  "type": "https://www.rfc-editor.org/rfc/rfc9457",
  "title": "Source access denied",
  "status": 403,
  "errorCode": "source_permission_denied",
  "sourceName": "orders",
  "traceId": "0HN7Q4E8M63KJ:00000001"
}

The source-level endpoint filter only checks the outer From source; foreign sources reachable via Subquery are independently authorized inside the validator. A bypassed outer-source filter (one that lets a request through despite source policy denying it) is surfaced as a 500 QueryAuthorizationConfigurationException whose diagnostic message survives in the response body, because the only path to that state is a configuration bug.

Discovery isRestricted flag

GET /sources/{name}/columns includes an isRestricted boolean on every column. When true:

  • Type-shape facts (dataType, isNullable, isCollection, isEnum, isPrimaryKey, displayName, collectionElementType) are preserved so UI builders can still render placeholders of the correct shape.
  • aliases, metadata, and children are suppressed (empty/null) — block-cascade defense so alternate paths and consumer hints are not leaked through a denied ancestor.

A query that filters or projects by a restricted column is rejected with the column denial response above. A query that does NOT touch any restricted column proceeds normally even if the schema declares restricted columns elsewhere.

IUserAccessor for non-HTTP consumers

Column authorization and row-level filters use IUserAccessor (registered by default) to read the current ClaimsPrincipal. AddQueryBuilderEndpoints() registers HttpContextUserAccessor (which pulls the principal from IHttpContextAccessor) and removes any AnonymousUserAccessor fallback that an earlier AddQueryBuilderEntityFramework() registered, so the HTTP-aware accessor wins regardless of helper order. A consumer that registers its own IUserAccessor keeps it — only the anonymous fallback is removed, never a custom accessor. Background jobs, admin tools, or other non-HTTP consumers that invoke IQueryExecutionService directly should register a custom IUserAccessor so a deterministic principal flows through column auth and row filters.

services.AddScoped<IUserAccessor, BackgroundJobUserAccessor>();

When no principal is available, the default behavior is to use an anonymous ClaimsPrincipal(). This typically denies any restricted column with 403.

Authorization

MapQueryBuilderEndpoints() returns IEndpointConventionBuilder, enabling standard ASP.NET Core authorization chaining.

Group-Level Authorization

Secure all UQB endpoints with a single call:

app.MapQueryBuilderEndpoints().RequireAuthorization();

Per-Source and Endpoint-Group Authorization

Use the configurator overload when you need different authorization behavior for:

  • specific query sources
  • unconfigured query sources (fallback)
  • schema registry endpoints
  • query validation endpoint
app.MapQueryBuilderEndpoints(
    endpoints =>
    {
        endpoints.RequireQueryPolicyForSource("coworkers", "coworkers:read");
        endpoints.RequireQueryPolicyForSource("devices", "devices:read");
        endpoints.RequireDefaultQueryPolicy("query:read"); // Fallback for unconfigured sources
        endpoints.ConfigureRegistryEndpoints(e => e.RequireAuthorization("schema:read"));
        endpoints.ConfigureValidationEndpoint(e => e.RequireAuthorization());
    },
    options =>
    {
        // Optional: override DI-registered options for this mapping
    });

What each configurator method does

Method Applies To Typical Use
RequireQueryPolicyForSource(sourceName, policyName) Query endpoints for one source: POST {RoutePrefix}/{sourceName}, POST {RoutePrefix}/{sourceName}/bulk/by-key, and POST {RoutePrefix}/{sourceName}/{id} Require a source-specific policy such as "devices:read"
RequireDefaultQueryPolicy(policyName) Query endpoints for sources not explicitly mapped with RequireQueryPolicyForSource Provide a safe fallback, such as "query:read"
ConfigureRegistryEndpoints(configure) Registry group endpoints under {RegistryRoutePrefix} Apply endpoint conventions like .RequireAuthorization("schema:read")
ConfigureValidationEndpoint(configure) Validation endpoint POST {RoutePrefix}/validate Require auth for query syntax validation

Query policy resolution order

For query execution endpoints, policy selection is:

  1. Source-specific policy from RequireQueryPolicyForSource(...)
  2. Fallback from RequireDefaultQueryPolicy(...)
  3. No configurator policy (group/global authorization may still apply if configured elsewhere)

sourceName matching for RequireQueryPolicyForSource(...) is case-insensitive. Calling RequireQueryPolicyForSource(...) or RequireDefaultQueryPolicy(...) multiple times for the same key overwrites the previous value (last-wins).

When to use each method

  • Use RequireQueryPolicyForSource(...) when different domains need different policies.
  • Use RequireDefaultQueryPolicy(...) when most sources share one baseline policy.
  • Use ConfigureRegistryEndpoints(...) when schema discovery should be locked behind separate permissions.
  • Use ConfigureValidationEndpoint(...) when query syntax checks should not be publicly callable.

ConfigureRegistryEndpoints(...) and ConfigureValidationEndpoint(...) are composable. Calling them multiple times chains conventions in registration order.

Source Authorization Filtering

ISourceAuthorizationFilter provides business-logic authorization for source discovery and query access:

public interface ISourceAuthorizationFilter
{
    ValueTask<bool> CanDiscoverAsync(string sourceName, ClaimsPrincipal user, CancellationToken cancellationToken);
    ValueTask<bool> CanQueryAsync(string sourceName, ClaimsPrincipal user, CancellationToken cancellationToken);
}
Where each method is enforced
Method Enforced On Behavior
CanDiscoverAsync Registry endpoints (/sources, /sources/{sourceName}, /sources/{sourceName}/columns/*, /summary) Effective discovery = CanDiscoverAsync \|\| CanQueryAsync. List/summary endpoints filter results; single-source endpoints return 404
CanQueryAsync Query endpoints (/{sourceName}, /{sourceName}/bulk/by-key, /{sourceName}/{id}) Returns 403 if user can discover the source; 404 otherwise
CanQuery implies CanDiscover

The framework enforces that CanQueryAsync returning true implicitly grants discovery access. Implementors of ISourceAuthorizationFilter do not need to ensure consistency between the two methods — the effective discoverability check is CanDiscoverAsync || CanQueryAsync. This means a source is visible in registry endpoints whenever either method returns true.

CanDiscoverAsync CanQueryAsync Registry Query
true true Visible Allowed
true false Visible 403 Forbidden
false true Visible (implicit) Allowed
false false Hidden / 404 404 Not Found
Dual-layer authorization on query endpoints

Query endpoints evaluate authorization in two sequential layers. Both must pass:

  1. ASP.NET Core policy evaluation — If a policy is configured for the source (via RequireQueryPolicyForSource or RequireDefaultQueryPolicy), that policy is evaluated using IPolicyEvaluator. The HttpContext is passed as the authorization resource, enabling resource-based policy handlers.
  2. Business-logic authorizationCanQueryAsync is always called, regardless of whether a policy is configured. This provides a hook for custom domain-specific access control (e.g., tenant isolation, row-level security decisions).

If no configurator is provided or no policy maps to the source, only CanQueryAsync is evaluated. Authorization failures return 403 Forbidden when the user can effectively discover the source (via CanDiscoverAsync or implied by CanQueryAsync). Authorization failures return 404 Not Found when the source is unknown or the user cannot discover it, preventing source enumeration.

Implementing a custom filter
public class TenantSourceAuthorizationFilter(ITenantContext tenantContext) : ISourceAuthorizationFilter
{
    public async ValueTask<bool> CanDiscoverAsync(
        string sourceName, ClaimsPrincipal user, CancellationToken cancellationToken)
    {
        return await tenantContext.CanAccessSourceAsync(sourceName, cancellationToken);
    }

    public async ValueTask<bool> CanQueryAsync(
        string sourceName, ClaimsPrincipal user, CancellationToken cancellationToken)
    {
        return await tenantContext.CanAccessSourceAsync(sourceName, cancellationToken);
    }
}

Register a custom implementation before AddQueryBuilderEndpoints():

services.AddScoped<ISourceAuthorizationFilter, TenantSourceAuthorizationFilter>();
services.AddQueryBuilderEndpoints();  // TryAddScoped won't overwrite your registration

The default AllowAllSourceAuthorizationFilter returns true for both methods on all sources.

Metadata-Based Source Authorization

UseMetadataSourceAuthorization(...) on QueryBuilderEndpointsOptions replaces the default AllowAllSourceAuthorizationFilter with MetadataSourceAuthorizationFilter, which reads access policies from DataSourceDefinition.AccessPolicy at runtime.

Registration
// Basic: uses access policies from DataSourceBuilder without permission evaluation
services.AddQueryBuilderEndpoints(options =>
{
    options.UseMetadataSourceAuthorization();
});

// With permission evaluator: enables RequirePermission policies
services.AddQueryBuilderEndpoints(options =>
{
    options.UseMetadataSourceAuthorization<MyPermissionEvaluator>();
});

// With options
services.AddQueryBuilderEndpoints(options =>
{
    options.UseMetadataSourceAuthorization(auth =>
    {
        auth.UnconfiguredSourceBehavior = UnconfiguredSourceBehavior.AllowAuthenticated;
        auth.ValidationMode = AuthorizationValidationMode.FailFast;
    });
});

If a custom ISourceAuthorizationFilter or IColumnAuthorizationService is already registered, UseMetadataSourceAuthorization(...) throws InvalidOperationException by default. Pass overrideExistingFilter: true to replace either.

Access policy evaluation

The filter evaluates DataSourceDefinition.AccessPolicy in order:

  1. Unknown source (not in schema registry) — deny
  2. Unconfigured (AccessPolicy.IsExplicit == false) — fallback to UnconfiguredSourceBehavior option
  3. Explicit policies — matched by AccessRequirement type:
DataSourceBuilder method AccessRequirement Behavior
(none — default) Deny (non-explicit) Governed by UnconfiguredSourceBehavior
DenyAccess() Deny (explicit) Always denied, ignores fallback
RequireAuthenticated() Authenticated Requires user.Identity.IsAuthenticated
RequirePermission("key") PermissionRequired Delegates to IPermissionEvaluator
Implementing IPermissionEvaluator

Sources configured with RequirePermission() delegate to your IPermissionEvaluator:

public class MyPermissionEvaluator(IMyAuthService authService) : IPermissionEvaluator
{
    public async ValueTask<bool> HasPermissionAsync(
        PermissionEvaluationContext context,
        ClaimsPrincipal user,
        CancellationToken cancellationToken)
    {
        // context.SourceName  — the source being accessed
        // context.Permission  — the permission key (e.g., "devices:read")
        // context.Operation   — Discover or Query
        return await authService.HasPermissionAsync(
            user, context.Permission, cancellationToken);
    }
}

If no IPermissionEvaluator is registered and a source requires a permission, access is denied (fail-closed) and a rate-limited warning is logged.

MetadataAuthorizationOptions
Property Type Default Description
UnconfiguredSourceBehavior UnconfiguredSourceBehavior Deny How to handle sources without an explicit AccessPolicy. Deny blocks all access; AllowAuthenticated permits authenticated users.
ValidationMode AuthorizationValidationMode Warn Startup validation behavior. None disables checks; Warn logs issues; FailFast throws on misconfiguration.
Startup validation

MetadataAuthorizationValidator runs as an IHostedService at startup (unless ValidationMode is None). It creates a DI scope to resolve ISchemaRegistry before running checks. It checks:

  1. Missing evaluator — Sources or columns require permissions but no IPermissionEvaluator is registered
  2. Fallback in useUnconfiguredSourceBehavior is not Deny (logs a warning as a reminder)
  3. Unconfigured sources — Sources without explicit access policies exist

In FailFast mode, condition 1 or 3 throws InvalidOperationException to prevent startup.

QueryableBuilder Extensions

The QueryBuilder.Endpoints.Extensions namespace provides an extension method on IQueryableBuilder that accepts an ExecuteQueryRequest directly, applying the same filter logic as the API endpoint:

using QueryBuilder.Endpoints.Extensions;

var queryable = await queryableBuilder.BuildQueryableAsync<Order>(
    "orders", request, cancellationToken);

Behavior

The extension method maps ExecuteQueryRequest fields to IQueryableBuilder parameters:

Request Field Mapped To
Query Shorthand filter
Where Structured filter
OrderBy Ordering definitions

The structured filter (Where) is validated before being passed to the builder, using the same validation as the API endpoint.

Fields intentionally ignored: Select, GroupBy, Limit, Offset, IncludeTotalCount, IncludeDebug. The caller handles those downstream on the returned IQueryable<T>.

Exception Handling

BuildQueryableAsync throws QueryValidationException and other query builder exceptions when input is invalid. The built-in query endpoints catch and map these to ProblemDetails responses. Custom endpoints using BuildQueryableAsync should add the QueryExceptionEndpointFilter to get the same behavior:

using QueryBuilder.Endpoints.Filters;

group.MapPost("/summary", GetSummary)
    .AddEndpointFilter<QueryExceptionEndpointFilter>();

The filter maps:

  • QueryValidationException → 400 ValidationProblem
  • ValidationException → 400 ValidationProblem
  • DataSourceNotFoundException → 404 Not Found
  • NoExecutionStrategyException → 400 Bad Request
  • QueryExecutionException → 400 Bad Request

Use Case

Apply the same filters as the API endpoint, then perform custom domain operations on the queryable:

public class OrderAnalyticsService(IQueryableBuilder queryableBuilder)
{
    public async Task<List<RegionRevenue>> GetRevenueByRegionAsync(
        ExecuteQueryRequest request, CancellationToken ct)
    {
        var queryable = await queryableBuilder.BuildQueryableAsync<Order>(
            "orders", request, ct);

        return await queryable
            .GroupBy(o => o.Region)
            .Select(g => new RegionRevenue
            {
                Region = g.Key,
                Total = g.Sum(o => o.Total),
                Count = g.Count()
            })
            .ToListAsync(ct);
    }
}

Service Interfaces

The package provides nine service interfaces that can be overridden:

  • IQueryExecutionService - Query execution against registered data sources
  • IRecordLookupService - Single record retrieval by primary key
  • IBulkLookupService - Bulk key lookup with batching and optional NDJSON streaming
  • IQueryValidationService - Shorthand query syntax validation
  • ISchemaIntrospectionService - Schema registry metadata retrieval
  • IDistinctValuesService - Distinct column value retrieval (enum reflection or synthetic query)
  • ISourceAuthorizationFilter - Controls source discoverability and query access
  • IColumnAuthorizationService - Column-level permission checks (default permit-all OpenColumnAuthorizationService; metadata-backed implementation registered by UseMetadataSourceAuthorization)
  • IUserAccessor - Resolves the current ClaimsPrincipal for auth checks (default HttpContextUserAccessor; replace for non-HTTP scopes)

All services are registered with TryAddScoped, allowing consumer overrides when registered before AddQueryBuilderEndpoints().

Unscoped (in-process) execution — ExecuteUnscopedAsync

IQueryExecutionService.ExecuteUnscopedAsync runs the same pipeline as ExecuteAsync but as the system, for in-process callers that run queries on the system's behalf rather than an end user's. HTTP endpoint code always uses ExecuteAsync.

It skips: source-level authorization, column-level authorization, and all row-level filter injection — including principal-independent filters such as soft-delete or tenant scoping. A query run this way therefore returns rows an HTTP caller would never see. The caller owns any scoping it still wants and must add it to the request Where; the injected filters (delegate RowFilterFactory or typed IRowFilter) are opaque to inspection, so there is no API to enumerate "what would have been applied." Do not point ExecuteUnscopedAsync at a source whose row filter encodes data partitioning (e.g. tenant isolation) unless you re-add that predicate yourself — otherwise a system query crosses the partition.

It keeps everything else: shorthand parsing, structured-filter validation, ordering/select/group-by validation, @-function resolution, and unregistered-source detection (an unregistered source — outer or subquery — still throws DataSourceNotFoundException). The full default selection is returned (no restricted-column omission).

@me and other principal-dependent functions resolve against whatever the host's feature providers supply for the current (typically anonymous) principal; with no UserContextFeature they surface a validation error.

// The service is scoped. An in-process caller with no HttpContext (e.g. a hosted/background
// service) creates its own scope; the resolved principal is anonymous, which is fine because
// ExecuteUnscopedAsync skips principal-dependent authorization. (A caller-supplied `@me` in the
// request still has no UserContextFeature to resolve against and surfaces a validation error.)
using var scope = serviceScopeFactory.CreateScope();
var queryExecutionService = scope.ServiceProvider.GetRequiredService<IQueryExecutionService>();
var results = await queryExecutionService.ExecuteUnscopedAsync("orders", request, ct);

Prerequisites

Requires QueryBuilder core services registered via:

builder.Services.AddQueryBuilder();

See QueryBuilder.Extensions for core service registration options.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.13-beta 38 6/3/2026
10.0.12-beta 48 6/1/2026
10.0.11-beta 46 5/31/2026
10.0.10-beta 51 5/28/2026
10.0.9-beta 49 5/27/2026
10.0.8-beta 57 5/18/2026
10.0.7-beta 53 5/16/2026
10.0.6-beta 53 5/11/2026
10.0.5-beta 52 4/30/2026
10.0.4-beta 54 4/23/2026
10.0.3-beta 74 4/23/2026
10.0.2-beta 61 4/10/2026
10.0.1-beta 55 4/10/2026
10.0.0-beta 57 4/9/2026