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) - [Quick Start](quick-start.md)
- [Dynamic Table API](dynamic-table-api.md) - [Dynamic Table API](dynamic-table-api.md)
- [Fluent Builder API](fluent-builder-api.md) - [Fluent Builder API](fluent-builder-api.md)
- [Typed Fluent Syntax](typed-fluent-syntax.md)
- [Mapping and Entities](mapping-and-entities.md) - [Mapping and Entities](mapping-and-entities.md)
- [Transactions and Disposal](transactions-and-disposal.md) - [Transactions and Disposal](transactions-and-disposal.md)
- [Stored Procedures](stored-procedures.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>()` - `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. 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 ## Insert, Update, Delete
```csharp ```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)