# Typed Fluent Syntax This page documents the new typed fluent syntax added alongside the original dynamic fluent builder API. It is intended for cases where: - you want property-based IntelliSense instead of raw column names - you want mapped property validation in fluent SQL construction - you still want explicit SQL composition instead of LINQ translation The old dynamic fluent API remains unchanged. This typed API is additive. ## Two Typed Styles There are currently two typed select styles: - positional typed builder: - `db.FromTyped("alias")` - scoped typed builder: - `db.FromTypedScope("alias")` Recommendation: - use `FromTypedScope(...)` for multi-join queries - use `FromTyped(...)` for simple root-table typed queries or when you want direct positional generic overloads ## Root Typed Builder Basic example: ```csharp var cmd = db.FromTyped("u") .SelectSql(u => u.Col(x => x.Code)) .WhereSql(u => u.Col(x => x.Id).Eq(1)); ``` Mapped properties are resolved through entity metadata: ```csharp [Table(Name = "sample_users")] public class User { [Column("id_user", true)] public long Id { get; set; } [Column("user_code")] public string Code { get; set; } } ``` So: - `u.Col(x => x.Id)` renders mapped column `u."id_user"` - `u.Col(x => x.Code)` renders mapped column `u."user_code"` ## Core Building Blocks The typed DSL is built around: - `TypedTableContext` - `TypedSqlExpression` - `TypedSqlPredicate` - `TypedSqlOrderExpression` - `Sql.*` helpers Common building blocks: ```csharp u.Col(x => x.Id) Sql.Val(1) Sql.Count() Sql.Coalesce(u.Col(x => x.Code), Sql.Val("N/A")) Sql.Case().When(...).Else(...) ``` ## Positional Typed Builder `FromTyped(...)` supports typed joins and then exposes joined tables by position. Example: ```csharp var cmd = db.FromTyped("u") .Join(j => j.Left().As("c").OnSql((u, c) => u.Col(x => x.Id).Eq(c.Col(x => x.UserId)))) .SelectSql( (u, c) => u.Col(x => x.Id).As("user_id"), (u, c) => c.Col(x => x.ClientCode).As("client_code")) .WhereSql((u, c) => c.Col(x => x.IsDeleted).Eq(0)); ``` This style is explicit, but becomes noisy as join count grows because join types must be repeated in generic parameters. ## Scoped Typed Builder `FromTypedScope(...)` solves the repeated-generic problem by changing builder type after each join. Example: ```csharp var cmd = db.FromTypedScope("u") .Join(j => j.Left().As("c").OnSql((u, c) => u.Col(x => x.Id).Eq(c.Col(x => x.UserId)))) .Join(j => j.Left().As("r").OnSql((u, c, r) => c.Col(x => x.UserId).Eq(r.Col(x => x.UserId)))) .SelectSql( (u, c, r) => u.Col(x => x.Id), (u, c, r) => r.Col(x => x.RoleName).As("role_name")) .WhereSql((u, c, r) => r.Col(x => x.RoleId).Gt(0)); ``` Important behavior: - each `Join(...)` extends the available typed lambda parameters - later joins can reference previously joined tables inside `OnSql(...)` - this is the preferred typed syntax for larger join graphs Current implementation supports up to `16` typed tables in one scoped chain. ## Join Syntax Typed joins support: - `Inner()` - `Join()` - `Left()` - `Right()` - `Full()` - `LeftOuter()` - `RightOuter()` - `FullOuter()` - `Type("...")` for custom join keywords - `NoLock()` - `OnSql(...)` - `OnRaw(...)` Examples: ```csharp .Join(j => j.Left().As("c").OnSql((u, c) => u.Col(x => x.Id).Eq(c.Col(x => x.UserId)))) .Join(j => j.Type("CROSS APPLY").As("c").NoLock()) .Join(j => j.Left().As("r").OnRaw("c.\"User_Id\" = r.\"User_Id\"")) ``` `OnSql(...)` is usually the best option because it stays mapped and typed. ## Selecting Columns Single-column select: ```csharp .SelectSql(u => u.Col(x => x.Code)) ``` Aliased select: ```csharp .SelectSql(u => u.Col(x => x.Code).As("user_code")) ``` Wildcard: ```csharp .SelectSql(u => u.All()) ``` Multi-table scoped select: ```csharp .SelectSql( (u, c, r) => u.Col(x => x.Id).As("user_id"), (u, c, r) => c.Col(x => x.ClientCode).As("client_code"), (u, c, r) => r.Col(x => x.RoleName).As("role_name")) ``` ## Predicates Available predicate patterns include: - `Eq` - `NotEq` - `Gt` - `Gte` - `Lt` - `Lte` - `IsNull` - `IsNotNull` - `Like` - `StartsWith` - `EndsWith` - `Contains` - `In` - `NotIn` - `Between` - `And` - `Or` - `Not` Example: ```csharp .WhereSql((u, c) => u.Col(x => x.Code).StartsWith("ADM") .And(c.Col(x => x.IsDeleted).Eq(0)) .And(c.Col(x => x.ClientCode).IsNotNull())) ``` ## Functions and Expressions Standard helpers: - `Sql.Count()` - `Sql.Count(expr)` - `Sql.Sum(...)` - `Sql.Avg(...)` - `Sql.Min(...)` - `Sql.Max(...)` - `Sql.Abs(...)` - `Sql.Upper(...)` - `Sql.Lower(...)` - `Sql.Trim(...)` - `Sql.Length(...)` - `Sql.NullIf(...)` - `Sql.Coalesce(...)` - `Sql.CurrentTimestamp()` Examples: ```csharp .SelectSql( u => Sql.Count().As("cnt"), u => Sql.Coalesce(u.Col(x => x.Code), Sql.Val("N/A")).As("code_value")) .HavingSql((u, c) => Sql.Count(c.Col(x => x.UserId)).Gt(0)) ``` Arithmetic is also supported: ```csharp .SelectSql(u => (u.Col(x => x.Id) + Sql.Val(1)).As("plus_one")) ``` ## CASE Expressions ```csharp .SelectSql(u => Sql.Case() .When(u.Col(x => x.Code).Eq("A"), "Alpha") .When(u.Col(x => x.Code).Eq("B"), "Beta") .Else("Other") .As("category")) ``` Use `CASE` when expression logic becomes too awkward for simple `COALESCE` or comparison predicates. ## Grouping and Ordering Grouping: ```csharp .GroupBySql((u, c) => u.Col(x => x.Id)) ``` Ordering: ```csharp .OrderBySql( (u, c) => c.Col(x => x.ClientCode).Asc(), (u, c) => Sql.Count(c.Col(x => x.UserId)).Desc()) ``` Null ordering helpers are also available: - `NullsFirst()` - `NullsLast()` Raw order fragments: ```csharp .OrderBySql(u => Sql.RawOrder("2 DESC")) ``` ## Custom SQL Escape Hatches For cases not covered by built-in helpers: - `Sql.Raw(...)` - `Sql.Func(...)` - `OnRaw(...)` Examples: ```csharp Sql.Raw("ROW_NUMBER() OVER(ORDER BY u.\"id_user\")") Sql.Func("CUSTOM_FUNC", u.Col(x => x.Code), Sql.Val(5)) ``` Use these when you need provider-specific or advanced SQL fragments, but keep normal query construction on typed helpers where possible. ## Scoped Subqueries Two subquery helper families exist: - classic typed helper: - `Sql.SubQuery(...)` - `Sql.Exists(...)` - scoped typed helper: - `Sql.SubQueryScope(...)` - `Sql.ExistsScope(...)` Use the scoped helpers when the subquery itself benefits from `FromTypedScope(...)`. Example: ```csharp .SelectSql(u => Sql.SubQueryScope( db, sq => sq .Join(j => j.Left().As("c").OnSql((x, c) => x.Col(a => a.Id).Eq(c.Col(a => a.UserId)))) .Join(j => j.Left().As("r").OnSql((x, c, r) => c.Col(a => a.UserId).Eq(r.Col(a => a.UserId)))) .SelectSql((x, c, r) => r.Col(a => a.RoleName)), "x").As("role_name")) ``` `ExistsScope(...)` works the same way, but returns a predicate. ## Modify Builders Typed SQL DSL also exists for modify builders: - `InsertTyped()` - `UpdateTyped()` - `DeleteTyped()` Examples: ```csharp db.UpdateTyped() .SetSql(u => u.Code, u => Sql.Coalesce(Sql.Val("900"), u.Col(x => x.Code))) .WhereSql(u => u.Col(x => x.Id).Eq(1)); db.InsertTyped() .InsertSql(u => u.Code, u => Sql.Val("901")); db.DeleteTyped() .WhereSql(u => u.Col(x => x.Id).Eq(2)); ``` ## Practical Guidance - prefer `FromTypedScope(...)` once a query has more than one or two joins - use `OnSql(...)` instead of `OnRaw(...)` whenever mappings exist - use `Sql.Func(...)` and `Sql.Raw(...)` sparingly as escape hatches - use `SubQueryScope(...)` / `ExistsScope(...)` when the subquery itself is multi-join and typed - keep the original dynamic fluent builder for cases where fully dynamic alias/member construction is still the better fit ## Related Pages - [Fluent Builder API](fluent-builder-api.md) - [Mapping and Entities](mapping-and-entities.md) - [Test-Driven Examples](test-driven-examples.md)