Files
DynamORM/docs/fluent-builder-api.md

7.5 KiB

Fluent Builder API

For the newer property-mapped typed fluent API, see Typed Fluent Syntax. 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

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:

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:

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:

.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

.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):

.Where(new DynamicColumn("u.Deleted").Eq(0).SetBeginBlock())
.Where(new DynamicColumn("u.IsActive").Eq(1).SetOr().SetEndBlock())

Object conditions + _table

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

.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(...)

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

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

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).