From 0afc894fd6f3e0815f9f476c21de95d8bfccbfc7 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 13:34:16 +0100 Subject: [PATCH] Add docs for typed fluent syntax --- docs/README.md | 3 + docs/quick-start.md | 16 ++ docs/typed-fluent-syntax.md | 367 ++++++++++++++++++++++++++++++++++++ 3 files changed, 386 insertions(+) create mode 100644 docs/typed-fluent-syntax.md diff --git a/docs/README.md b/docs/README.md index 025deea..fd7fe6c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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() / db.Update() / db.Delete()` 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). diff --git a/docs/quick-start.md b/docs/quick-start.md index 4a24707..9f3b3d4 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.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("u") + .SelectSql(u => u.Col(x => x.Code)) + .WhereSql(u => u.Col(x => x.Id).Eq(19)); + + var value = cmd.ScalarAs(); + 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 diff --git a/docs/typed-fluent-syntax.md b/docs/typed-fluent-syntax.md new file mode 100644 index 0000000..e8b4968 --- /dev/null +++ b/docs/typed-fluent-syntax.md @@ -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("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)