This is not a built-in LiteOrm query syntax. It is an integration pattern: the frontend sends filters through the query string, and the backend converts those parameters into LiteOrm Expr before running the query.
It works best when filters are relatively stable and the UI benefits from shareable, refreshable, back-button-friendly URLs.
| Scenario | Recommended approach | Why |
|---|---|---|
| Simple list filtering | QueryString | Easy to debug and share |
| Back-office list pages with fixed filters | QueryString | Lower frontend/backend complexity |
| Complex nested logic and dynamic condition groups | Native Expr | QueryString becomes too limited |
| Refresh/back navigation should keep filters | QueryString | State is visible in the URL |
Before exposing this kind of endpoint, align on three rules:
ExprIf the UI already needs grouped AND / OR logic, multi-column dynamic sorting, or a visual condition builder, switch to frontend native Expr instead.
An endpoint such as GET /api/orders/query commonly uses:
| Parameter | Purpose |
|---|---|
keyword |
Matches order number, customer name, and product name |
status |
Order status |
departmentName |
Department name contains |
createdByUserName |
Creator display name contains |
minTotalAmount / maxTotalAmount |
Amount range |
createdFrom / createdTo |
Created time range |
sortBy |
Sort field |
desc |
Descending flag |
page / pageSize |
Paging parameters |
onlyMine |
Force “my orders only” |
URLSearchParamsconst params = new URLSearchParams();
params.set("keyword", "Contoso");
params.set("status", "Pending");
params.set("sortBy", "CreatedTime");
params.set("desc", "true");
params.set("page", "1");
params.set("pageSize", "5");
const result = await demoApp.apiFetch(`/api/orders/query?${params.toString()}`);
To load summary data, you can reuse the same filter set with:
const stats = await demoApp.apiFetch(`/api/orders/stats?${params.toString()}`);
The frontend only transports parameters. The backend should still centralize the actual query rules, including:
keyword, ranges, and sorting parameters into ExprThat keeps list, stats, and export endpoints aligned on the same query behavior.
If a list API always returns total, the backend usually needs an extra Count query. When the same filters and pages are requested repeatedly, that count query is a good candidate for short-lived caching.
One practical approach for a demo-style project is:
Count result, not the page dataExpr object itself as the cache key, together with the current cache version for validity checksA practical pattern looks like this:
Expr directly as the IMemoryCache keyEquals/GetHashCode implementation for cache hitsIMemoryCache firstCountAsync(...) and store the resultThis keeps count caching scoped to “the same user + the same effective filter” and avoids cross-user reuse without converting the filter into JSON first.
total, not the actual page itemsThe query API returns:
| Field | Meaning |
|---|---|
page / pageSize |
Current page and page size |
total |
Total matching record count |
items |
Current page items |
sql |
Latest executed SQL |
The stats API returns aggregate values plus SQL.
QueryString is only a transport format. Authorization is still enforced on the backend:
admin can view all dataonlyMine=true lets an admin intentionally narrow to self-owned dataURLSearchParams.total and therefore breaking paging UX.