266 lines
7.5 KiB
Markdown
266 lines
7.5 KiB
Markdown
# Fluent Builder API
|
|
|
|
For the newer property-mapped typed fluent API, see [Typed Fluent Syntax](typed-fluent-syntax.md). This page focuses on the original dynamic fluent builder semantics and parser behavior.
|
|
|
|
The fluent API is built around:
|
|
|
|
- `IDynamicSelectQueryBuilder`
|
|
- `IDynamicInsertQueryBuilder`
|
|
- `IDynamicUpdateQueryBuilder`
|
|
- `IDynamicDeleteQueryBuilder`
|
|
|
|
This page documents call semantics and parser nuances from:
|
|
|
|
- `DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs`
|
|
- `DynamORM/Builders/Implementation/DynamicQueryBuilder.cs`
|
|
- `DynamORM/Builders/Extensions/DynamicWhereQueryExtensions.cs`
|
|
- `DynamORM/Builders/Extensions/DynamicHavingQueryExtensions.cs`
|
|
- parser tests in `DynamORM.Tests/Select/ParserTests.cs` and `LegacyParserTests.cs`
|
|
|
|
## Builder Lifecycle and Clause Output Order
|
|
|
|
Builder methods can be chained in many orders, but generated SQL is emitted in canonical order:
|
|
|
|
1. `SELECT`
|
|
2. `FROM`
|
|
3. `JOIN`
|
|
4. `WHERE`
|
|
5. `GROUP BY`
|
|
6. `HAVING`
|
|
7. `ORDER BY`
|
|
8. paging (`TOP`/`FIRST SKIP` or `LIMIT OFFSET` depending on options)
|
|
|
|
Recommendation: start with `From(...)` first so alias registration is unambiguous for later clauses.
|
|
|
|
## Fast Baseline
|
|
|
|
```csharp
|
|
using (var query = db.From("users", "u")
|
|
.Where("u.id", 19)
|
|
.SelectColumn("u.first", "u.last"))
|
|
{
|
|
var rows = query.Execute().ToList();
|
|
}
|
|
```
|
|
|
|
## `From(...)` Deep Dive
|
|
|
|
Supported source forms:
|
|
|
|
- string source:
|
|
- `From(x => "dbo.Users")`
|
|
- `From(x => "dbo.Users AS u")`
|
|
- member source:
|
|
- `From(x => x.dbo.Users)`
|
|
- `From(x => x.dbo.Users.As(x.u))`
|
|
- typed source:
|
|
- `From(x => x(typeof(User)).As(x.u))`
|
|
- `db.From<User>("u")`
|
|
- subquery source:
|
|
- `From(x => x(subQuery).As(x.u))`
|
|
|
|
Aliasing nuance:
|
|
|
|
- `As(...)` is optional for simple table/member source.
|
|
- For generic invoke source (`x => x(expression)`), aliasing is effectively required when the source should be referenced later.
|
|
|
|
`NoLock()` nuance:
|
|
|
|
- `NoLock()` is parsed in `From`/`Join`, but SQL `WITH(NOLOCK)` is emitted only if database option `SupportNoLock` is enabled.
|
|
|
|
Examples from tests:
|
|
|
|
```csharp
|
|
cmd.From(x => x.dbo.Users.As(x.u));
|
|
cmd.From(x => x("\"dbo\".\"Users\"").As(x.u));
|
|
cmd.From(x => x(cmd.SubQuery().From(y => y.dbo.Users)).As(x.u));
|
|
cmd.From(x => x.dbo.Users.As("u").NoLock());
|
|
```
|
|
|
|
## `Join(...)` Deep Dive
|
|
|
|
`Join(...)` supports table/member/string/invoke/subquery forms with `On(...)` conditions.
|
|
|
|
### Two-pass join processing
|
|
|
|
Implementation runs join parsing in two passes:
|
|
|
|
- pass 1: collect tables/aliases
|
|
- pass 2: render SQL conditions
|
|
|
|
This enables robust alias resolution inside `On(...)` expressions.
|
|
|
|
### Join type resolution
|
|
|
|
Join type comes from a dynamic method near the root of the join expression.
|
|
|
|
Common forms:
|
|
|
|
- `Inner()` -> `INNER JOIN`
|
|
- `Left()` -> `LEFT JOIN`
|
|
- `LeftOuter()` -> `LEFT OUTER JOIN`
|
|
- `Right()` -> `RIGHT JOIN`
|
|
- `RightOuter()` -> `RIGHT OUTER JOIN`
|
|
- no type method -> plain `JOIN`
|
|
|
|
Non-obvious option:
|
|
|
|
- the join-type method can accept arguments.
|
|
- if first argument is `false`, auto-append/split of `JOIN` is disabled.
|
|
- if any argument is a string, that string is used as explicit join type text.
|
|
|
|
This allows custom forms such as provider-specific joins.
|
|
|
|
Example patterns:
|
|
|
|
```csharp
|
|
cmd.Join(x => x.Inner().dbo.UserClients.As(x.uc).On(x.u.Id == x.uc.UserId));
|
|
|
|
cmd.Join(x => x.Custom(false, "CROSS APPLY")
|
|
.dbo.UserClients.As(x.uc)
|
|
.On(x.u.Id == x.uc.UserId));
|
|
```
|
|
|
|
### `On(...)` ordering rule
|
|
|
|
`On(...)` can appear with other join modifiers in the same lambda chain. Parsed order is normalized by node traversal.
|
|
|
|
Recommended readable order:
|
|
|
|
```csharp
|
|
.Join(x => x.Inner().dbo.UserClients.As(x.uc).NoLock().On(x.u.Id == x.uc.UserId))
|
|
```
|
|
|
|
Validated join variants are heavily covered in parser tests (`TestJoinClassic`, `TestInnerJoin*`, `TestLeftJoin`, `TestRightJoin`, `TestSubQueryJoin`).
|
|
|
|
## `Where(...)` and `Having(...)` Nuances
|
|
|
|
Both clauses share the same behavior model.
|
|
|
|
### Lambda conditions
|
|
|
|
```csharp
|
|
.Where(x => x.u.UserName == "admin")
|
|
.Where(x => x.Or(x.u.IsActive == true))
|
|
```
|
|
|
|
Chaining behavior:
|
|
|
|
- each chained call defaults to `AND`
|
|
- root wrapper `Or(...)` switches that clause append to `OR`
|
|
|
|
### `DynamicColumn` behavior
|
|
|
|
`DynamicColumn` supports:
|
|
|
|
- comparison operators (`Eq`, `Not`, `Like`, `NotLike`, `In`, `Between`, etc.)
|
|
- `SetOr()` to force OR join with previous condition
|
|
- `SetBeginBlock()` / `SetEndBlock()` to control parenthesis grouping
|
|
- `SetVirtualColumn(...)` to influence null parameterization behavior
|
|
|
|
Example (legacy parser tests):
|
|
|
|
```csharp
|
|
.Where(new DynamicColumn("u.Deleted").Eq(0).SetBeginBlock())
|
|
.Where(new DynamicColumn("u.IsActive").Eq(1).SetOr().SetEndBlock())
|
|
```
|
|
|
|
### Object conditions + `_table`
|
|
|
|
```csharp
|
|
.Where(new { Deleted = 0, IsActive = 1, _table = "u" })
|
|
```
|
|
|
|
Nuance:
|
|
|
|
- `_table` applies alias/table prefix during object-condition expansion.
|
|
- when `schema: true`, only key-like mapped columns are included.
|
|
|
|
`Having(...)` mirrors these forms and semantics.
|
|
|
|
## `Select(...)` Nuances
|
|
|
|
Supported forms:
|
|
|
|
- simple member: `Select(x => x.u.UserName)`
|
|
- aliasing: `Select(x => x.u.UserName.As(x.Name))`
|
|
- all columns: `Select(x => x.u.All())`
|
|
- aggregate functions: `Select(x => x.Sum(x.u.Score).As(x.Total))`
|
|
- anonymous projection: `Select(x => new { Id = x.u.Id, Name = x.u.UserName })`
|
|
- raw invoke concatenation: `Select(x => x("CASE WHEN ", x.u.Active == 1, " THEN ", 1, " ELSE ", 0, " END").As(x.Flag))`
|
|
|
|
Non-obvious behavior:
|
|
|
|
- anonymous projection property names become SQL aliases.
|
|
- `All()` cannot be combined with alias emission for that same select item.
|
|
- invoke syntax (`x("...", expr, "...")`) is the parser escape hatch for complex SQL expressions.
|
|
|
|
## `OrderBy(...)` Nuances
|
|
|
|
Default direction is ascending.
|
|
|
|
```csharp
|
|
.OrderBy(x => x.u.UserName) // ASC
|
|
.OrderBy(x => x.Desc(x.u.UserName)) // DESC
|
|
.OrderBy(x => "1 DESC") // numeric column position
|
|
.OrderBy(x => x.Desc(1)) // numeric column position
|
|
```
|
|
|
|
`OrderByColumn(...)` also supports `DynamicColumn` parsing style, including numeric alias position patterns from legacy tests.
|
|
|
|
## `GroupBy(...)`
|
|
|
|
```csharp
|
|
.GroupBy(x => x.u.UserName)
|
|
// or
|
|
.GroupByColumn("u.Name")
|
|
```
|
|
|
|
Group-by supports multiple entries by repeated calls or params arrays.
|
|
|
|
## Paging and Distinct
|
|
|
|
- `Distinct(true)` toggles `SELECT DISTINCT`.
|
|
- `Top(n)` delegates to `Limit(n)`.
|
|
- `Limit(n)` and `Offset(n)` validate database capability flags.
|
|
|
|
Capability checks:
|
|
|
|
- `Limit` requires one of: `SupportLimitOffset`, `SupportFirstSkip`, `SupportTop`.
|
|
- `Offset` requires one of: `SupportLimitOffset`, `SupportFirstSkip`.
|
|
|
|
Unsupported combinations throw `NotSupportedException`.
|
|
|
|
## Subqueries
|
|
|
|
Subqueries can be embedded in select/where/join contexts.
|
|
|
|
```csharp
|
|
cmd.From(x => x.dbo.Users.As(x.u))
|
|
.Select(x => x(cmd.SubQuery()
|
|
.From(y => y.dbo.AccessRights.As(y.a))
|
|
.Where(y => y.a.User_Id == y.u.Id)
|
|
.Select(y => y.a.IsAdmin)).As(x.IsAdmin));
|
|
```
|
|
|
|
Supported tested placements:
|
|
|
|
- `SELECT (subquery) AS alias`
|
|
- `WHERE column = (subquery)`
|
|
- `WHERE column IN (subquery)`
|
|
- `JOIN (subquery) AS alias ON ...`
|
|
|
|
## Typed Execution and Scalar
|
|
|
|
```csharp
|
|
var list = db.From<User>("u").Where(x => x.u.Active == true).Execute<User>().ToList();
|
|
var count = db.From<User>("u").Select(x => x.Count()).ScalarAs<int>();
|
|
```
|
|
|
|
## Practical Patterns
|
|
|
|
- Register source aliases early (`From(...)` first).
|
|
- Keep join lambdas readable with explicit `Inner/Left/Right` and `On(...)` at the end.
|
|
- Use invoke syntax only for cases the parser cannot express directly.
|
|
- Use legacy `DynamicColumn` API when you need explicit grouping flags (`SetBeginBlock`, `SetEndBlock`, `SetOr`).
|