Querying

June 10, 2026 · View on GitHub

DataSurface provides a rich query system for filtering, sorting, searching, paginating, and projecting fields — all driven by the ResourceContract.


Pagination

All list responses are paginated. Pagination is mandatory and cannot be disabled.

ParameterExampleDefaultDescription
page?page=21Page number (1-based)
pageSize?pageSize=5020Items per page

Maximum page size is controlled per resource via MaxPageSize on [CrudResource] (default: 200). Requests exceeding the maximum are clamped to the maximum.

Response Format

{
  "items": [...],
  "page": 1,
  "pageSize": 20,
  "total": 142
}

Response Headers

List endpoints include pagination headers:

X-Total-Count: 142
X-Page: 1
X-Page-Size: 20

Filtering

Filter results using filter[field]=operator:value query parameters. Only fields with the CrudDto.Filter flag can be filtered.

Operators

OperatorExampleDescription
eqfilter[status]=eq:activeEquals (default if operator omitted)
neqfilter[status]=neq:deletedNot equals
gtfilter[price]=gt:100Greater than
gtefilter[price]=gte:100Greater than or equal
ltfilter[price]=lt:50Less than
ltefilter[price]=lte:50Less than or equal
containsfilter[name]=contains:johnString contains (case-insensitive)
startsfilter[name]=starts:johnString starts with
endsfilter[name]=ends:sonString ends with
infilter[status]=in:a|b|cIn list (pipe-separated values)
isnullfilter[email]=isnull:trueIs null / is not null

When the operator is omitted, eq is assumed:

?filter[status]=active       # same as filter[status]=eq:active

Only the whitelisted operator prefixes above are treated as operators. A value whose prefix is not a known operator is parsed as a plain equality value, so values that naturally contain : (ISO timestamps, URNs, ns:key strings) work without escaping:

?filter[createdAt]=2026-01-01T00:00:00Z   # plain equality, not torn at the colon

Filter Errors

The following return HTTP 400 with the offending field in errors:

  • An unparseable filter value for the field's type (e.g. filter[price]=gt:abc)
  • An operator unsupported for the field's type (e.g. contains on a number, gt on a bool)
  • isnull on a non-nullable field

Multiple Filters

Multiple filters are combined with AND logic:

?filter[status]=active&filter[price]=gt:100

This returns items where status = active AND price > 100.

Enabling Filtering on Fields

[CrudField(CrudDto.Read | CrudDto.Filter)]
public string Status { get; set; } = default!;

[CrudField(CrudDto.Read | CrudDto.Filter | CrudDto.Sort)]
public decimal Price { get; set; }

Filters on non-filterable fields are silently ignored.


Sorting

Sort results using the sort query parameter. Only fields with the CrudDto.Sort flag can be sorted.

?sort=title,-createdAt
  • Comma-separated field names
  • Prefix with - for descending order
  • Multiple sort fields are applied in order (primary, secondary, etc.)

Default Sort

Set a default sort order per resource:

[CrudResource("posts", DefaultSort = "-createdAt")]
public class Post { /* ... */ }

The default sort applies when no sort parameter is provided. Every field referenced by DefaultSort must be sortable (CrudDto.Sort) — this is validated at startup, and an invalid DefaultSort fails contract validation.

Sorts on non-sortable fields are silently ignored.

Deterministic Ordering

Every paged query has a guaranteed deterministic order: the requested sort wins, falling back to the contract's DefaultSort, and the key is always appended as a final tie-breaker (or used as the only ordering when no sort applies). Unsorted list queries are therefore ordered by key — pages never repeat or skip rows.


Search across multiple fields using the q parameter:

?q=john

This searches all fields marked with Searchable = true:

[CrudField(CrudDto.Read | CrudDto.Filter, Searchable = true)]
public string Title { get; set; } = default!;

[CrudField(CrudDto.Read, Searchable = true)]
public string Description { get; set; } = default!;

The search performs case-insensitive LIKE '%term%' across all searchable fields, combined with OR logic. Search can be combined with filters:

?q=john&filter[status]=active

Field Projection

Select specific fields to return using the fields parameter:

?fields=id,email,name

Response

{
  "items": [
    { "id": 1, "email": "john@example.com", "name": "John" },
    { "id": 2, "email": "jane@example.com", "name": "Jane" }
  ]
}
  • Comma-separated list of field API names
  • Only requested fields are included in the response
  • Invalid field names are ignored
  • Works with both list (GET /api/{resource}) and single (GET /api/{resource}/{id}) endpoints
  • Requires EnableFieldProjection = true in feature flags (enabled by default)

Expansion

Include related resources using the expand parameter:

?expand=author,tags

Configuration

Relations must be explicitly marked as expandable:

[CrudRelation(ReadExpandAllowed = true)]
public User Author { get; set; } = default!;

Depth Limit

Maximum expansion depth is configurable per resource:

[CrudResource("posts", MaxExpandDepth = 2)]
public class Post { /* ... */ }

Default depth limit is 1.

Default Expansion

Relations can be expanded automatically without requesting them:

[CrudRelation(ReadExpandAllowed = true, DefaultExpanded = true)]
public User Author { get; set; } = default!;

See Relationships for full relation configuration.


Combining Query Parameters

All query parameters can be combined:

GET /api/posts?page=1&pageSize=25&sort=-createdAt&filter[status]=published&q=tutorial&expand=author&fields=id,title,author

This request:

  1. Filters to published posts
  2. Searches for "tutorial" in searchable fields
  3. Sorts by newest first
  4. Expands the author relation
  5. Returns only id, title, and author fields
  6. Returns page 1 with 25 items per page