Add docs for typed fluent syntax
This commit is contained in:
@@ -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).
|
||||||
|
|||||||
@@ -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
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