This document is part of the Extension Integration Guide: when the frontend no longer needs “just a few fixed filter options” but needs to dynamically combine fields, operators, sorting, and pagination, you can submit LiteOrm’s native Expr JSON directly.
The recommended approach is: the frontend constructs JSON according to the actual output shape of JsonSerializer.Serialize<Expr>(...), rather than inventing a separate DSL just for frontend use.
| Scenario | Recommended Approach | Reason |
|---|---|---|
| Dynamic multi-condition query | Native Expr | Fields, operators, and values can all be combined at runtime |
| Switchable AND / OR | Native Expr | Easier to express complex logic |
| Multi-column sorting | Native Expr | OrderBys directly supports multiple columns |
| Custom pagination | Native Expr | Skip / Take natively available |
When submitting Expr directly from the frontend, it is recommended to establish these three conventions first:
When LiteOrm serializes SectionExpr -> OrderByExpr -> WhereExpr, the output shape is approximately:
{
"$section": {
"$orderby": {
"$where": null,
"Where": {
"$": "and",
"Items": [
{
"$": "==",
"Left": { "#": "Status" },
"Right": { "@": "Pending" }
},
{
"$": ">=",
"Left": { "#": "TotalAmount" },
"Right": { "@": 300 }
}
]
}
},
"OrderBys": [
{
"Field": { "#": "CreatedTime" },
"Asc": false
}
]
},
"Skip": 0,
"Take": 5
}
Key points:
$section represents its SourceSkip / Take are at the same level$orderby represents its SourceOrderBys are at the same level$where represents its Source; can be null if no upstream segmentWhere is at the same levelconst logicExpr = {
"$": "and",
"Items": [
{ "$": "==", "Left": { "#": "Status" }, "Right": { "@": "Pending" } },
{ "$": ">=", "Left": { "#": "TotalAmount" }, "Right": { "@": 300 } }
]
};
WhereExpr Serialization Resultlet expr = {
"$where": null,
"Where": logicExpr
};
OrderByExpr Serialization Resultexpr = {
"$orderby": expr,
"OrderBys": [
{ "Field": { "#": "CreatedTime" }, "Asc": false },
{ "Field": { "#": "TotalAmount" }, "Asc": true }
]
};
SectionExpr Serialization Resultexpr = {
"$section": expr,
"Skip": 0,
"Take": 5
};
const payload = {
"$section": {
"$orderby": {
"$where": null,
"Where": {
"$": "contains",
"Left": { "#": "CustomerName" },
"Right": { "@": "Contoso" }
}
},
"OrderBys": [
{ "Field": { "#": "CreatedTime" }, "Asc": false }
]
},
"Skip": 0,
"Take": 5
};
const result = await demoApp.apiFetch("/api/orders/query/expr", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
The backend typically receives this JSON as Expr, then parses out:
Then supplements permission filtering. For non-Admin users, the backend automatically appends:
using static LiteOrm.Common.Expr;
Prop(nameof(DemoOrder.CreatedByUserId)) == currentUser.Id
Native Expr queries also frequently need to return total, so it’s also suitable for adding short-term caching to Count.
A suitable approach for the demo project:
ExprExpr directly as the count cache keyLiteOrm’s Expr already implements structured Equals/GetHashCode, so native Expr filter conditions with the same structure can reuse the same count result during continuous pagination without needing to convert to JSON first for key generation.
OrderBy, Skip, Take do not affect the total count, so you can cache count based only on the final filter conditions"$": "section" / Source DirectlyIt is recommended to stay consistent with LiteOrm’s actual serialization output rather than wrapping in an extra custom structure.
Skip / Take Inside the $section ObjectThey should be at the same level as $section, not inside $section’s value.
OrderBys Inside $orderby’s ValueOrderBys should be at the same level as $orderby; $orderby’s value only represents its Source.