LiteOrm在线文档

Frontend Native Expr Query

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.

Choosing the Right Approach

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

1. Integration Principles

When submitting Expr directly from the frontend, it is recommended to establish these three conventions first:

2. Current Actual JSON Shape

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:

  1. The value of $section represents its Source
  2. Skip / Take are at the same level
  3. The value of $orderby represents its Source
  4. OrderBys are at the same level
  5. The value of $where represents its Source; can be null if no upstream segment
  6. Where is at the same level

3. Frontend Construction Steps

3.1 First Generate Logic Expression

const logicExpr = {
    "$": "and",
    "Items": [
        { "$": "==", "Left": { "#": "Status" }, "Right": { "@": "Pending" } },
        { "$": ">=", "Left": { "#": "TotalAmount" }, "Right": { "@": 300 } }
    ]
};

3.2 Construct WhereExpr Serialization Result

let expr = {
    "$where": null,
    "Where": logicExpr
};

3.3 Construct OrderByExpr Serialization Result

expr = {
    "$orderby": expr,
    "OrderBys": [
        { "Field": { "#": "CreatedTime" }, "Asc": false },
        { "Field": { "#": "TotalAmount" }, "Asc": true }
    ]
};

3.4 Wrap as SectionExpr Serialization Result

expr = {
    "$section": expr,
    "Skip": 0,
    "Take": 5
};

4. JavaScript Call Example

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)
});

5. What the Backend Does

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

5.1 Count Caching

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:

LiteOrm’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.

5.2 Caveats

6. Common Mistakes

6.1 Writing "$": "section" / Source Directly

It is recommended to stay consistent with LiteOrm’s actual serialization output rather than wrapping in an extra custom structure.

6.2 Putting Skip / Take Inside the $section Object

They should be at the same level as $section, not inside $section’s value.

6.3 Putting OrderBys Inside $orderby’s Value

OrderBys should be at the same level as $orderby; $orderby’s value only represents its Source.