# 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("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("u").Where(x => x.u.Active == true).Execute().ToList(); var count = db.From("u").Select(x => x.Count()).ScalarAs(); ``` ## 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`).