Add docs for typed fluent syntax

This commit is contained in:
root
2026-02-27 13:34:16 +01:00
parent 3797505c9c
commit 0afc894fd6
3 changed files with 386 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ This documentation is based on:
- [Quick Start](quick-start.md)
- [Dynamic Table API](dynamic-table-api.md)
- [Fluent Builder API](fluent-builder-api.md)
- [Typed Fluent Syntax](typed-fluent-syntax.md)
- [Mapping and Entities](mapping-and-entities.md)
- [Transactions and Disposal](transactions-and-disposal.md)
- [Stored Procedures](stored-procedures.md)
@@ -36,3 +37,5 @@ DynamORM provides two major usage styles:
- `db.Insert<T>() / db.Update<T>() / db.Delete<T>()`
The dynamic API is concise and fast to use. The fluent builder API gives stronger composition and explicit SQL control.
For the newer property-mapped typed fluent syntax, see [Typed Fluent Syntax](typed-fluent-syntax.md).

View File

@@ -52,6 +52,22 @@ using (var query = db.From("users").Where("id", 19).SelectColumn("first"))
}
```
## First Query (Typed Fluent Syntax)
```csharp
using (var db = new DynamicDatabase(SQLiteFactory.Instance, "Data Source=app.db;", options))
{
var cmd = db.FromTyped<User>("u")
.SelectSql(u => u.Col(x => x.Code))
.WhereSql(u => u.Col(x => x.Id).Eq(19));
var value = cmd.ScalarAs<string>();
Console.WriteLine(value);
}
```
For multi-join typed queries, prefer `FromTypedScope(...)`. Full details are documented in [Typed Fluent Syntax](typed-fluent-syntax.md).
## Insert, Update, Delete
```csharp

367
docs/typed-fluent-syntax.md Normal file
View File

@@ -0,0 +1,367 @@
# 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<T>("alias")`
- scoped typed builder:
- `db.FromTypedScope<T>("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<User>("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<T>`
- `TypedSqlExpression<T>`
- `TypedSqlPredicate`
- `TypedSqlOrderExpression`
- `Sql.*` helpers
Common building blocks:
```csharp
u.Col(x => x.Id)
Sql.Val(1)
Sql.Count()
Sql.Coalesce<string>(u.Col(x => x.Code), Sql.Val("N/A"))
Sql.Case<string>().When(...).Else(...)
```
## Positional Typed Builder
`FromTyped(...)` supports typed joins and then exposes joined tables by position.
Example:
```csharp
var cmd = db.FromTyped<User>("u")
.Join<UserClient>(j => j.Left().As("c").OnSql((u, c) =>
u.Col(x => x.Id).Eq(c.Col(x => x.UserId))))
.SelectSql<UserClient>(
(u, c) => u.Col(x => x.Id).As("user_id"),
(u, c) => c.Col(x => x.ClientCode).As("client_code"))
.WhereSql<UserClient>((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<User>("u")
.Join<UserClient>(j => j.Left().As("c").OnSql((u, c) =>
u.Col(x => x.Id).Eq(c.Col(x => x.UserId))))
.Join<UserRole>(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<TJoin>(...)` 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<UserClient>(j => j.Left().As("c").OnSql((u, c) =>
u.Col(x => x.Id).Eq(c.Col(x => x.UserId))))
.Join<UserClient>(j => j.Type("CROSS APPLY").As("c").NoLock())
.Join<UserRole>(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<string>(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<string>()
.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<T>(...)`
- `Sql.Func<T>(...)`
- `OnRaw(...)`
Examples:
```csharp
Sql.Raw<int>("ROW_NUMBER() OVER(ORDER BY u.\"id_user\")")
Sql.Func<string>("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<TModel, TResult>(...)`
- `Sql.Exists<TModel>(...)`
- scoped typed helper:
- `Sql.SubQueryScope<TModel, TResult>(...)`
- `Sql.ExistsScope<TModel>(...)`
Use the scoped helpers when the subquery itself benefits from `FromTypedScope(...)`.
Example:
```csharp
.SelectSql(u => Sql.SubQueryScope<User, string>(
db,
sq => sq
.Join<UserClient>(j => j.Left().As("c").OnSql((x, c) =>
x.Col(a => a.Id).Eq(c.Col(a => a.UserId))))
.Join<UserRole>(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<T>()`
- `UpdateTyped<T>()`
- `DeleteTyped<T>()`
Examples:
```csharp
db.UpdateTyped<User>()
.SetSql(u => u.Code, u => Sql.Coalesce<string>(Sql.Val("900"), u.Col(x => x.Code)))
.WhereSql(u => u.Col(x => x.Id).Eq(1));
db.InsertTyped<User>()
.InsertSql(u => u.Code, u => Sql.Val("901"));
db.DeleteTyped<User>()
.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)