Add docs for typed fluent syntax
This commit is contained in:
367
docs/typed-fluent-syntax.md
Normal file
367
docs/typed-fluent-syntax.md
Normal 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)
|
||||
Reference in New Issue
Block a user