7.3 KiB
Fluent Builder API
The fluent API is built around:
IDynamicSelectQueryBuilderIDynamicInsertQueryBuilderIDynamicUpdateQueryBuilderIDynamicDeleteQueryBuilder
This page documents call semantics and parser nuances from:
DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.csDynamORM/Builders/Implementation/DynamicQueryBuilder.csDynamORM/Builders/Extensions/DynamicWhereQueryExtensions.csDynamORM/Builders/Extensions/DynamicHavingQueryExtensions.cs- parser tests in
DynamORM.Tests/Select/ParserTests.csandLegacyParserTests.cs
Builder Lifecycle and Clause Output Order
Builder methods can be chained in many orders, but generated SQL is emitted in canonical order:
SELECTFROMJOINWHEREGROUP BYHAVINGORDER BY- paging (
TOP/FIRST SKIPorLIMIT OFFSETdepending 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 inFrom/Join, but SQLWITH(NOLOCK)is emitted only if database optionSupportNoLockis 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 JOINLeft()->LEFT JOINLeftOuter()->LEFT OUTER JOINRight()->RIGHT JOINRightOuter()->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 ofJOINis 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 toOR
DynamicColumn behavior
DynamicColumn supports:
- comparison operators (
Eq,Not,Like,NotLike,In,Between, etc.) SetOr()to force OR join with previous conditionSetBeginBlock()/SetEndBlock()to control parenthesis groupingSetVirtualColumn(...)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:
_tableapplies 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)togglesSELECT DISTINCT.Top(n)delegates toLimit(n).Limit(n)andOffset(n)validate database capability flags.
Capability checks:
Limitrequires one of:SupportLimitOffset,SupportFirstSkip,SupportTop.Offsetrequires 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 aliasWHERE 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/RightandOn(...)at the end. - Use invoke syntax only for cases the parser cannot express directly.
- Use legacy
DynamicColumnAPI when you need explicit grouping flags (SetBeginBlock,SetEndBlock,SetOr).