Expr is LiteOrm’s core object-expression model,and this article mainly explains how to construct, compose, reuse, and understand its semantics.
If your question is when to choose Lambda, Expr, or ExprString, continue with the Query Guide.
using static LiteOrm.Common.Expr;
var age = Prop("Age");
var userName = Prop("U", "UserName");
var paramValue = Value(18); // parameterized
var constValue = Const("Enabled"); // inlined
Prop(name): create a property expressionProp(alias, name): create a property expression with a table aliasValue(obj): parameterized runtime valueConst(obj): inline SQL constantusing static LiteOrm.Common.Expr;
var expr1 = Prop("Age") >= 18;
var expr2 = Prop("DeptId").In(1, 2, 3);
var expr3 = Prop("Age").Between(18, 30);
var expr4 = Prop("UserName").Contains("admin");
var expr5 = Prop("UserName").Like("%root%");
All of these return LogicExpr, so they can keep composing.
using static LiteOrm.Common.Expr;
var absAge = Func("ABS", Prop("Age"));
var countExpr = Aggregate("COUNT", Prop("Id"), isDistinct: true);
var currentUserFilter = Sql("CurrentUserFilter");
Func(name, args): regular SQL functionAggregate(name, expr, isDistinct): aggregate wrapperSql(key, arg): registered dynamic SQL fragment for runtime-context filtersExistsLambda style:
var users = await userService.SearchAsync(
u => Exists<Department>(d => d.Id == u.DeptId && d.Name == "R&D")
);
Expr style:
using static LiteOrm.Common.Expr;
var expr = Exists<Department>(
Prop("Id") == Prop("T0", "DeptId")
& Prop("Name") == "R&D"
);
Use this when you want to write the correlation condition explicitly.
ExistsRelatedLambda style:
var users = await userService.SearchAsync(
u => ExistsRelated<DepartmentView>(d => d.Name == "R&D")
);
Expr style:
using static LiteOrm.Common.Expr;
var expr = ExistsRelated<DepartmentView>(
Prop("Name") == "R&D"
);
ExistsRelated fills in the relation condition from metadata such as ForeignType and TableJoin.
For the detailed matching rules, see Associations.
using static LiteOrm.Common.Expr;
LogicExpr condition = null;
if (minAge.HasValue)
condition &= Prop("Age") >= minAge.Value;
if (deptId.HasValue)
condition &= Prop("DeptId") == deptId.Value;
if (!string.IsNullOrWhiteSpace(keyword))
condition &= Prop("UserName").Contains(keyword);
& and | are null-friendly, which makes them ideal for admin search filters.
using static LiteOrm.Common.Expr;
public static LogicExpr BuildUserSearch(IReadOnlyDictionary<string, string?> query)
{
LogicExpr condition = null;
if (query.TryGetValue("minAge", out var minAgeText) && int.TryParse(minAgeText, out var minAge))
condition &= Prop("Age") >= minAge;
if (query.TryGetValue("keyword", out var keyword) && !string.IsNullOrWhiteSpace(keyword))
condition &= Prop("UserName").Contains(keyword);
return condition;
}
This works well for open query endpoints, gateway forwarding, and frontend query builders.
using static LiteOrm.Common.Expr;
LogicExpr extra = null;
extra &= Prop("UserName").Contains("John");
var users = await userService.SearchAsync(
u => u.IsActive == true && extra.To<bool>()
);
If you want Lambda readability outside and dynamic Expr reuse inside, continue with Mixing Lambda and Expr.
Expr.From<T>()using static LiteOrm.Common.Expr;
var query = From<User>()
.Where(Prop("Age") > 18)
.GroupBy(Prop("DeptId"))
.Having(Prop("Id").Count() > 5)
.Select(
Prop("DeptId"),
Prop("Id").Count().As("UserCount")
)
.OrderBy(Prop("UserCount").Desc())
.Section(0, 20);
This is the most complete Expr style: start from FROM, then build WHERE / GROUP BY / HAVING / SELECT / ORDER BY / paging.
You can think of LiteOrm’s Expr model as four layers:
| Layer | Representative types | Purpose |
|---|---|---|
| Root | Expr |
The common base type for all expression objects |
| Value expressions | ValueTypeExpr |
Values that can appear in columns, functions, comparisons, and SELECT items |
| Logical expressions | LogicExpr |
Boolean expressions that can appear in WHERE, HAVING, or EXISTS |
| SQL segments | SqlSegment |
Chainable SQL nodes such as FROM / SELECT / WHERE / ORDER BY |
ValueExpr: literal or parameter valuePropertyExpr: column referenceFunctionExpr: function callValueBinaryExpr: value arithmetic such as a + bUnaryExpr: unary operations such as -a or DISTINCT aValueSet: a value set such as IN (...)SelectItemExpr: SELECT xxx AS AliasOrderByItemExpr: ORDER BY xxx ASC/DESCLogicBinaryExpr: comparisons such as Age >= 18AndExpr: AND compositionOrExpr: OR compositionNotExpr: NOT compositionForeignExpr: the EXISTS expression used by Exists and ExistsRelatedLambdaExpr: a wrapper used during Lambda conversion; usually not written by handSourceExpr: abstract base for SQL segments that can act as a data sourceTableExpr: tableCommonTableExpr: CTETableJoinExpr: JOINFromExpr: FROMSelectExpr: SELECTWhereExpr: WHEREGroupByExpr: GROUP BYHavingExpr: HAVINGOrderByExpr: ORDER BYSectionExpr: pagingUpdateExpr: UPDATEDeleteExpr: DELETEIn day-to-day query code, the most common ones are usually:
PropertyExpr / ValueExprLogicBinaryExpr / AndExpr / OrExprForeignExprSelectExpr / WhereExpr / OrderByExpr| Method | Description | Example |
|---|---|---|
Expr.Prop(name) |
Create a property expression | Expr.Prop("Age") |
Expr.Prop(alias, name) |
Create a property expression with alias | Expr.Prop("U", "UserName") |
Expr.Value(value) |
Create a parameterized value | Expr.Value(18) |
Expr.Const(value) |
Create an inline constant | Expr.Const("Enabled") |
Expr.Null |
SQL NULL | Expr.Null |
Expr.From<T>() |
Create a chained-query starting point | Expr.From<User>() |
Expr.Update<T>() |
Create an UPDATE expression | Expr.Update<User>() |
Expr.Delete<T>() |
Create a DELETE expression | Expr.Delete<User>() |
Expr.Exists<T>(innerExpr) |
Create an EXISTS subquery | Expr.Exists<Department>(...) |
Expr.ExistsRelated<T>(innerExpr) |
Create an auto-related EXISTS subquery | Expr.ExistsRelated<DepartmentView>(...) |
Expr.Lambda<T>(expr) |
Convert Lambda into LogicExpr |
Expr.Lambda<User>(u => u.Age > 18) |
Expr.Func(name, args) |
Create a function expression | Expr.Func("COUNT", Expr.Prop("Id")) |
Expr.Aggregate(name, expr, isDistinct) |
Create an aggregate expression | Expr.Aggregate("COUNT", Expr.Prop("Id"), true) |
Expr.If(condition, then, else) |
IF / CASE WHEN form | Expr.If(... ) |
Expr.Case(cases, elseExpr) |
CASE expression | Expr.Case(... ) |
Expr.Now() |
Current timestamp | Expr.Now() |
Expr.Today() |
Current date | Expr.Today() |
Expr.Sql(key, arg) |
Dynamic SQL fragment | Expr.Sql("CurrentUserFilter") |
Expr.Query<T>(expression) |
Convert IQueryable Lambda to Expr | Expr.Query<User>(...) |
Expr.Query<T, TResult>(expression) |
Convert IQueryable Lambda with scalar result to Expr | Expr.Query<User, int>(...) |
LiteOrm overloads common C# operators on ValueTypeExpr and LogicExpr, so many expressions read very close to ordinary code:
| Operator | Applies to | Returns | Example |
|---|---|---|---|
== != > < >= <= |
ValueTypeExpr |
LogicExpr |
Prop("Age") >= 18 |
+ - * / % |
ValueTypeExpr |
ValueTypeExpr |
Prop("Amount") * 0.9m |
unary - / ~ |
ValueTypeExpr |
ValueTypeExpr |
-Prop("Balance"), ~Prop("Flags") |
& \| |
LogicExpr |
LogicExpr |
(Prop("Age") >= 18) & (Prop("Status") == 1) |
! |
LogicExpr |
LogicExpr |
!(Prop("IsDeleted") == true) |
For example:
using static LiteOrm.Common.Expr;
var scoreExpr = (Prop("MathScore") + Prop("ExtraScore")) / 2;
var filter = (Prop("Age") >= 18 & Prop("Status") == 1)
| Prop("UserName").Contains("admin");
+, use .Concat(...)When hand-writing Expr, ValueTypeExpr’s + operator has arithmetic-add semantics, and may render SQL +, which is not portable for string concatenation across databases.
Use concat explicitly:
using static LiteOrm.Common.Expr;
var fullName = Prop("FirstName")
.Concat(" ")
.Concat(Prop("LastName"));
Concat(...) is rendered via SqlBuilder.BuildConcatSql, so providers can output CONCAT(a,b,...) or a || b as appropriate.
Note: In Lambda expressions (e.g.
SearchAsync(u => u.FirstName + " " + u.LastName == "..." )), C# string+is typically converted to concat during parsing; but when hand-writingExpr, always use.Concat(...)explicitly.
LogicExpr& and | on LogicExpr are especially useful for dynamic filters because they are null-friendly:
using static LiteOrm.Common.Expr;
LogicExpr condition = null;
condition &= Prop("Age") >= 18;
condition &= Prop("Status") == 1;
condition |= Prop("IsVip") == true;
The rules are:
null & expr => exprexpr & null => exprnull | expr => exprexpr | null => exprThis removes the need to manually guard every composition step with a null check.
ValueTypeExpr / ValueExpr support implicit conversion from:
stringintlongboolDateTimedoubledecimalSo you can write:
using static LiteOrm.Common.Expr;
var expr1 = Prop("Age") >= 18;
var expr2 = Prop("CreateTime") >= DateTime.Today;
var expr3 = Prop("IsEnabled") == true;
var expr4 = Prop("Amount") + 12.5m;
Those literals are automatically wrapped as ValueExpr, which usually means the same as:
Prop("Age") >= Value(18)
Prop("CreateTime") >= Value(DateTime.Today)
Ordinary literals used through operator overloads follow Expr.Value(...) semantics, not Expr.Const(...) semantics:
using static LiteOrm.Common.Expr;
var expr = Prop("Status") == 1; // parameterized
var constExpr = Prop("Status") == Const(1); // inlined constant
Rule of thumb:
Value(...)Const(...)null does not have an implicit conversionnull is not implicitly converted into ValueTypeExpr, so null checks should be written explicitly:
using static LiteOrm.Common.Expr;
var expr1 = Prop("DeletedTime").IsNull();
var expr2 = Prop("DeletedTime") == Expr.Null;
For database semantics, .IsNull() / .IsNotNull() is usually clearer.
LiteOrm also includes a few ergonomic conversions for chained APIs:
using static LiteOrm.Common.Expr;
var query = From<User>()
.OrderBy(("Age", false)); // (string property, bool ascending) -> OrderByItemExpr
var update = Update<User>()
.Set((Prop("Age"), Prop("Age") + 1)); // (PropertyExpr, ValueTypeExpr) -> SetItem
These are mainly about reducing ceremony so OrderBy(...), Set(...), and similar APIs stay concise.
Tip: when mixing comparison, arithmetic, and logical operators, add parentheses instead of relying on C# operator precedence to make the final expression obvious.
| Method | Description | Example |
|---|---|---|
& / .And(right) |
AND | Prop("Age") > 18 & Prop("DeptId") == 2 |
| / .Or(right) |
OR | condition1 | condition2 |
! / .Not() |
NOT | !Prop("IsDeleted").Equal(true) |
| Method | Description |
|---|---|
.Equal(v) .NotEqual(v) |
equals / not equals |
.GreaterThan(v) .LessThan(v) |
greater / less |
.GreaterThanOrEqual(v) .LessThanOrEqual(v) |
greater-or-equal / less-or-equal |
.In(params items) .In(IEnumerable) .In(Expr) |
IN set / subquery |
.Between(low, high) |
BETWEEN |
| Method | Description |
|---|---|
.Like(pattern) |
LIKE |
.Contains(text) .StartsWith(text) .EndsWith(text) |
common string predicates |
.RegexpLike(pattern) |
regex predicate |
.IsNull() .IsNotNull() |
NULL checks |
.IfNull(defaultValue) |
null replacement |
| Method | Description |
|---|---|
.As(name) |
create SelectItemExpr |
.Distinct() |
DISTINCT |
.Count() .Sum() .Avg() .Max() .Min() |
aggregates |
.Asc() .Desc() |
ordering |
.Over(partitionBy) |
window function |
| Method | Description |
|---|---|
.Where(condition) |
WHERE |
.GroupBy(props) |
GROUP BY |
.Having(condition) |
HAVING |
.Select(props) |
SELECT |
.OrderBy(props) |
ORDER BY |
.Section(skip, take) |
paging |
.Set(assignments) |
UPDATE SET |
Expression objects such as PropertyExpr, TableExpr, ForeignExpr, FunctionExpr, SelectExpr, SelectItemExpr, CommonTableExpr, and GenericSqlExpr treat names and aliases as case-insensitive in Equals / GetHashCode.
For example:
Expr.Prop("User", "Name")
Expr.Prop("user", "name")
are treated as equal expressions.
AndExpr / OrExpr use set semanticsAndExpr.Items and OrExpr.Items now use set semantics:
Equals / GetHashCode no longer depend on duplicate distributionSo:
new AndExpr(a, a, b)
new AndExpr(a, b)
are equivalent in composition semantics.