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.
| Parameter | Example | Default | Description |
|---|---|---|---|
page | ?page=2 | 1 | Page number (1-based) |
pageSize | ?pageSize=50 | 20 | Items 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
| Operator | Example | Description |
|---|---|---|
eq | filter[status]=eq:active | Equals (default if operator omitted) |
neq | filter[status]=neq:deleted | Not equals |
gt | filter[price]=gt:100 | Greater than |
gte | filter[price]=gte:100 | Greater than or equal |
lt | filter[price]=lt:50 | Less than |
lte | filter[price]=lte:50 | Less than or equal |
contains | filter[name]=contains:john | String contains (case-insensitive) |
starts | filter[name]=starts:john | String starts with |
ends | filter[name]=ends:son | String ends with |
in | filter[status]=in:a|b|c | In list (pipe-separated values) |
isnull | filter[email]=isnull:true | Is 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.
containson a number,gton a bool) isnullon 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.
Full-Text Search
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 = truein 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:
- Filters to published posts
- Searches for "tutorial" in searchable fields
- Sorts by newest first
- Expands the author relation
- Returns only id, title, and author fields
- Returns page 1 with 25 items per page
Related
- Validation — How input validation works
- Relationships — Expansion and relation writes
- API Endpoints Reference — Full endpoint specification