From b628348af884ae262478d8f84ab7c585c10e8f3d Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 08:06:56 +0100 Subject: [PATCH] Integrate deep-dive content into API docs and expand mapping --- README.md | 1 - docs/README.md | 1 - docs/dynamic-table-api.md | 44 +++++- docs/fluent-builder-api.md | 46 ++++++- docs/mapping-and-entities.md | 127 ++++++++++++++--- docs/stored-procedures.md | 105 ++++++++++---- docs/syntax-and-procedures-deep-dive.md | 173 ------------------------ 7 files changed, 267 insertions(+), 230 deletions(-) delete mode 100644 docs/syntax-and-procedures-deep-dive.md diff --git a/README.md b/README.md index b1def78..7463a88 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ Full documentation is available in [`docs/README.md`](docs/README.md): - Quick start - Dynamic table API - Fluent builder API -- Syntax and procedure deep dive - Mapping and entity lifecycle - Transaction/disposal semantics - Stored procedures diff --git a/docs/README.md b/docs/README.md index d5b2993..025deea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,6 @@ This documentation is based on: - [Quick Start](quick-start.md) - [Dynamic Table API](dynamic-table-api.md) - [Fluent Builder API](fluent-builder-api.md) -- [Syntax and Procedure Deep Dive](syntax-and-procedures-deep-dive.md) - [Mapping and Entities](mapping-and-entities.md) - [Transactions and Disposal](transactions-and-disposal.md) - [Stored Procedures](stored-procedures.md) diff --git a/docs/dynamic-table-api.md b/docs/dynamic-table-api.md index 61ec861..21db32b 100644 --- a/docs/dynamic-table-api.md +++ b/docs/dynamic-table-api.md @@ -1,11 +1,13 @@ # Dynamic Table API -The dynamic table API centers on `DynamicTable` and allows expressive calls like: +The dynamic table API centers on `DynamicTable` and allows concise runtime calls. ```csharp dynamic users = db.Table("users"); ``` +This API is best when table/column selection is dynamic or you want very short CRUD code. + ## Read Operations Examples backed by `DynamORM.Tests/Select/DynamicAccessTests.cs`: @@ -23,6 +25,14 @@ users.Single(id: 19); users.Query(columns: "first,last", order: "id:desc"); ``` +## Filtering with Named Arguments + +```csharp +users.Count(first: "Ori"); +users.Single(code: "101"); +users.Query(columns: "id,first", id: 19); +``` + ## Conditions with `DynamicColumn` ```csharp @@ -30,18 +40,32 @@ users.Count(where: new DynamicColumn("id").Greater(100)); users.Count(where: new DynamicColumn("login").Like("Hoyt.%")); users.Count(where: new DynamicColumn("id").Between(75, 100)); users.Count(where: new DynamicColumn("id").In(75, 99, 100)); +users.Count(where: new DynamicColumn("id").In(new[] { 75, 99, 100 })); +``` + +Using aggregate expressions in a condition: + +```csharp +users.Count(condition1: new DynamicColumn +{ + ColumnName = "email", + Aggregate = "length", + Operator = DynamicColumn.CompareOperator.Gt, + Value = 27 +}); ``` ## Insert ```csharp -users.Insert(code: "201", first: "Juri", last: "Gagarin"); +users.Insert(code: "201", first: "Juri", last: "Gagarin", email: "juri.gagarin@megacorp.com"); users.Insert(values: new { code = "202", first = "Juri", - last = "Gagarin" + last = "Gagarin", + email = "juri.gagarin@megacorp.com" }); ``` @@ -69,10 +93,24 @@ users.Delete(where: new { id = 14, code = 14 }); ```csharp users.Count(type: typeof(User), columns: "id"); users.Query(type: typeof(User)); + +var list = (users.Query(type: typeof(User)) as IEnumerable) + .Cast() + .ToList(); ``` These usage patterns are covered in `DynamORM.Tests/Select/TypedAccessTests.cs`. +## When to Prefer Fluent Builder Instead + +Use fluent builders when: + +- Query structure is complex (joins/subqueries/having). +- You need deterministic SQL text assertions. +- You prefer strongly typed lambda parser expressions. + +See [Fluent Builder API](fluent-builder-api.md). + ## Notes - Dynamic member names map to table/column names and builder conventions. diff --git a/docs/fluent-builder-api.md b/docs/fluent-builder-api.md index 57985d7..49c8e82 100644 --- a/docs/fluent-builder-api.md +++ b/docs/fluent-builder-api.md @@ -1,6 +1,13 @@ # Fluent Builder API -The fluent API is built around interfaces like `IDynamicSelectQueryBuilder`, `IDynamicInsertQueryBuilder`, `IDynamicUpdateQueryBuilder`, and `IDynamicDeleteQueryBuilder`. +The fluent API is built around interfaces like: + +- `IDynamicSelectQueryBuilder` +- `IDynamicInsertQueryBuilder` +- `IDynamicUpdateQueryBuilder` +- `IDynamicDeleteQueryBuilder` + +This API is best when SQL structure must be explicit and composable. ## Core Select Flow @@ -26,10 +33,11 @@ using (var query = db.From("u") ## Parser Lambda Patterns -The parser supports patterns tested in `DynamORM.Tests/Select/ParserTests.cs`: +The parser supports patterns tested in `DynamORM.Tests/Select/ParserTests.cs` and `DynamORM.Tests/Select/LegacyParserTests.cs`: - `From(x => x.dbo.Users)` - `From(x => x.dbo.Users.As(x.u))` +- `From(x => "dbo.Users AS u")` - `Join(x => x.Left().Accounts.As(x.a).On(x.a.userId == x.u.id))` - `Where(x => x.Or(x.u.id > 100))` - `Select(x => x.u.first.As(x.firstName))` @@ -37,10 +45,28 @@ The parser supports patterns tested in `DynamORM.Tests/Select/ParserTests.cs`: - `Having(x => x.Count() > 1)` - `OrderBy(x => x.u.id.Desc())` +## Joins and Projection Example + +```csharp +using (var query = db.From("users", "u") + .Join(x => x.Left().profiles.As(x.p).On(x.p.user_id == x.u.id)) + .Select( + x => x.u.id, + x => x.u.first.As(x.firstName), + x => x.p.city.As(x.city)) + .Where(x => x.u.id > 10) + .OrderBy(x => x.u.id.Desc())) +{ + var rows = query.Execute().ToList(); +} +``` + ## Subqueries ```csharp -var sub = new DynamicSelectQueryBuilder(db).From(x => x.dbo.Users); +var sub = new DynamicSelectQueryBuilder(db) + .From(x => x.dbo.Users) + .Where(x => x.id > 100); using (var query = new DynamicSelectQueryBuilder(db) .From(x => x(sub).As("u")) @@ -61,7 +87,10 @@ var count = db.From() ## Modify Builders ```csharp -db.Insert("users").Values("code", "301").Values("first", "Ada").Execute(); +db.Insert("users") + .Values("code", "301") + .Values("first", "Ada") + .Execute(); db.Update("users") .Values("first", "Alicia") @@ -73,6 +102,15 @@ db.Delete("users") .Execute(); ``` +Typed variant: + +```csharp +db.Insert() + .Values(x => x.code, "302") + .Values(x => x.first, "Grace") + .Execute(); +``` + ## SQL Inspection You can inspect generated SQL from builder objects: diff --git a/docs/mapping-and-entities.md b/docs/mapping-and-entities.md index 319617e..1288e1e 100644 --- a/docs/mapping-and-entities.md +++ b/docs/mapping-and-entities.md @@ -1,8 +1,26 @@ # Mapping and Entities -DynamORM supports attribute-driven mapping and entity lifecycle helpers. +DynamORM mapping is a core feature, not just a convenience layer. -## Mapping Attributes +It controls: + +- class-to-table resolution +- property-to-column resolution +- key semantics for updates/deletes +- schema override details (`DbType`, size, precision, scale) +- lifecycle operations (`Insert`, `Update`, `Delete`, `Refresh`, `Save`) + +## Mapping Resolution Model + +At runtime, `DynamicMapperCache` builds and caches `DynamicTypeMap` for each type. + +`DynamicTypeMap` is used by: + +- typed query execution (`Execute()`) +- dynamic projection to classes +- entity lifecycle operations in `DynamicEntityBase` +- procedure result mapping +- validation (`ValidateObject`) ## `TableAttribute` @@ -14,9 +32,11 @@ public class User } ``` +Fields: + - `Name`: table name override. - `Owner`: schema/owner segment. -- `Override`: prefer attribute schema info over provider schema. +- `Override`: prefer attribute schema over database schema (important when provider schema is limited). ## `ColumnAttribute` @@ -28,6 +48,9 @@ public class User [Column("email")] public string Email { get; set; } + + [Column("created_at", false, DbType.DateTime)] + public DateTime CreatedAt { get; set; } } ``` @@ -39,13 +62,40 @@ Important flags: - `IsNoUpdate` - `Type`, `Size`, `Precision`, `Scale` -## Ignore Fields +These influence generated parameters and update/insert behavior. -Use `IgnoreAttribute` to skip properties in mapper-driven workflows. +## Advanced Mapping Example -## Validation +```csharp +[Table(Name = "users", Override = true)] +public class UserEntity : DynamicEntityBase +{ + [Column("id", true, DbType.Int32)] + public int Id { get; set; } -`RequiredAttribute` can define value rules. + [Column("code", false, DbType.String, 32)] + public string Code { get; set; } + + [Column("email", false, DbType.String, 256)] + public string Email { get; set; } + + [Column("created_at", false, DbType.DateTime)] + [Ignore] + public DateTime CreatedAtInternal { get; set; } +} +``` + +## Ignore and Partial Mapping + +Use `[Ignore]` when: + +- property is computed locally +- property is not persisted +- property should not participate in auto update/insert + +## Validation (`RequiredAttribute`) + +`RequiredAttribute` can enforce range and null/empty semantics. ```csharp public class Profile @@ -63,33 +113,65 @@ var issues = DynamicMapperCache.GetMapper().ValidateObject(profile); Validation scenarios are verified in `DynamORM.Tests/Helpers/Validation/ObjectValidationTest.cs`. -## `DynamicEntityBase` +## `DynamicEntityBase` Lifecycle `DynamicEntityBase` tracks state and changed fields. +State-driven `Save(db)` behavior: + +- `New` => `Insert` +- `Existing` + modified => `Update` +- `ToBeDeleted` => `Delete` +- `Deleted` => throws for write operations +- `Unknown` => throws (explicit state needed) + +Example entity with property change tracking: + ```csharp public class UserEntity : DynamicEntityBase { + private string _first; + [Column("id", true)] public int Id { get; set; } [Column("first")] - public string First { get; set; } + public string First + { + get => _first; + set + { + OnPropertyChanging(nameof(First), _first, value); + _first = value; + } + } } ``` -Available behaviors: +Typical flow: -- `Validate()` -- `Insert(db)` -- `Update(db)` -- `Delete(db)` -- `Refresh(db)` -- `Save(db)` driven by `DynamicEntityState` +```csharp +var user = db.From() + .Where(x => x.id == 19) + .Execute() + .First(); -## Repository Base +user.SetDynamicEntityState(DynamicEntityState.Existing); +user.First = "Yuri"; +user.Save(db); // updates only changed fields when possible +``` -`DynamicRepositoryBase` provides common operations: +## Refresh and Key Requirements + +`Refresh(db)`, `Update(db)`, and `Delete(db)` rely on key columns. + +If no key mapping is available, entity update/delete operations throw. + +Always map at least one key (`IsKey = true`) for mutable entities. + +## Repository Layer (`DynamicRepositoryBase`) + +`DynamicRepositoryBase` provides: - `GetAll()` - `GetByQuery(...)` @@ -98,4 +180,11 @@ Available behaviors: - `Delete(...)` - `Save(...)` -It ensures query/table compatibility for `T` unless explicitly bypassed. +It validates that query tables match mapped type `T` before executing typed enumeration. + +## Practical Recommendations + +- Always define keys explicitly in mapped models. +- Use `Override = true` when provider schema metadata is weak or inconsistent. +- Keep entities focused on persistence fields; use `[Ignore]` for non-persisted members. +- Use validation on boundary objects before persisting. diff --git a/docs/stored-procedures.md b/docs/stored-procedures.md index 05c1ba4..4ddeabd 100644 --- a/docs/stored-procedures.md +++ b/docs/stored-procedures.md @@ -2,8 +2,6 @@ Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled. -For an in-depth comparison with query syntax styles and advanced return-shape behavior, see [Syntax Deep Dive and Procedure Calls](syntax-and-procedures-deep-dive.md). - ## Basic Invocation ```csharp @@ -11,26 +9,7 @@ var scalar = db.Procedures.sp_Exp_Scalar(); var scalarTyped = db.Procedures.sp_Exp_Scalar(); ``` -## Input, Output, Return Parameters - -Prefixes in argument names control parameter direction: - -- `out_` for output -- `ret_` for return value -- `both_` for input/output - -Example pattern: - -```csharp -var res = db.Procedures.sp_Exp_SomeInputAndOutput< - string, - MyProcResult>( - Name: "G4g4r1n", - out_Result: new DynamicSchemaColumn { Size = 256 }, - ret_Return: 0); -``` - -## Nested Procedure Names +## Schema-Qualified Invocation Dynamic member chaining builds qualified names: @@ -40,14 +19,82 @@ var res = db.Procedures.dbo.reporting.MyProc(); This resolves to `dbo.reporting.MyProc`. -## Result Mapping +## Input, Output, Return, InputOutput Parameters -If generic return types are provided, DynamORM attempts mapper-based projection into the target type. +Prefixes in argument names control parameter direction: -If output parameters are present, result payload is assembled from: +- `out_` for output +- `ret_` for return value +- `both_` for input/output -- main scalar/resultset-derived value -- output values -- optional return value +Example: -The behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs` and documented in XML examples in `DynamORM/DynamicDatabase.cs`. +```csharp +dynamic res = db.Procedures.sp_Test_Scalar_In_Out( + inp: Guid.NewGuid(), + out_outp: Guid.Empty, + ret_Return: 0); +``` + +## Using `DynamicSchemaColumn` for Explicit Output Shape + +```csharp +var res = db.Procedures.sp_Exp_SomeInputAndOutput( + Name: "G4g4r1n", + out_Result: new DynamicSchemaColumn + { + Name = "Result", + Size = 256 + }, + ret_Return: 0); +``` + +## Using `DynamicColumn` for Direction + Value + Schema + +```csharp +var res = db.Procedures.sp_WithInputOutput( + both_State: new DynamicColumn + { + ColumnName = "State", + ParameterDirection = ParameterDirection.InputOutput, + Value = "pending", + Schema = new DynamicSchemaColumn { Name = "State", Size = 32 } + }); +``` + +## Result Shapes + +From `DynamicProcedureInvoker` behavior: + +- `T == IDataReader`: returns `CachedReader()` result. +- `T == DataTable`: materializes via `ToDataTable(...)`. +- `T == IEnumerable`: dynamic row enumeration. +- `T == IEnumerable`: converts first column of each row. +- `T == IEnumerable`: maps rows via mapper cache. +- `T == class`: maps structured result to a class. + +Examples: + +```csharp +IDataReader rdr = db.Procedures.MyProc(); +DataTable table = db.Procedures.MyProc(); +List ids = db.Procedures.MyProc>(); +List users = db.Procedures.MyProc>(); +User user = db.Procedures.MyProc(); +``` + +## Output and Return Value Aggregation + +When output and/or return values are used, DynamORM aggregates: + +- main result +- output parameters +- return value + +into a dynamic object or mapped class (if a target type is provided). + +## Notes + +- Enable `DynamicDatabaseOptions.SupportStoredProcedures` in options. +- Prefix stripping is automatic in result keys (`out_Result` becomes `Result`). +- Behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs` and XML examples in `DynamORM/DynamicDatabase.cs`. diff --git a/docs/syntax-and-procedures-deep-dive.md b/docs/syntax-and-procedures-deep-dive.md deleted file mode 100644 index e377b9c..0000000 --- a/docs/syntax-and-procedures-deep-dive.md +++ /dev/null @@ -1,173 +0,0 @@ -# Syntax Deep Dive and Procedure Calls - -This guide focuses on the two query syntaxes and advanced stored procedure invocation patterns. - -## Two Query Syntax Styles - -DynamORM supports: - -- Dynamic table syntax: concise runtime calls via `dynamic`. -- Fluent builder syntax: explicit SQL composition via interfaces and lambda parser. - -## Dynamic Table Syntax - -Entry point: - -```csharp -dynamic users = db.Table("users"); -``` - -Typical reads: - -```csharp -var count = users.Count(columns: "id"); -var first = users.First(columns: "id,first,last"); -var one = users.Single(id: 19); -var list = (users.Query(columns: "id,first", order: "id:desc") as IEnumerable).ToList(); -``` - -Dynamic filters with `DynamicColumn`: - -```csharp -users.Count(where: new DynamicColumn("id").Greater(100)); -users.Count(where: new DynamicColumn("last").In("Hendricks", "Goodwin", "Freeman")); -``` - -Modifications: - -```csharp -users.Insert(code: "201", first: "Juri", last: "Gagarin"); -users.Update(values: new { first = "Yuri" }, where: new { code = "201" }); -users.Delete(code: "201"); -``` - -Coverage references: - -- `DynamORM.Tests/Select/DynamicAccessTests.cs` -- `DynamORM.Tests/Modify/DynamicModificationTests.cs` - -## Fluent Builder Syntax - -Entry points: - -```csharp -var q1 = db.From("users", "u"); -var q2 = db.From("u"); -``` - -Composable select: - -```csharp -using (var query = db.From("u") - .Where(x => x.u.id > 10) - .Select(x => x.u.id, x => x.u.first) - .OrderBy(x => x.u.id.Desc())) -{ - var rows = query.Execute().ToList(); -} -``` - -Parser forms supported by tests: - -- `From(x => x.dbo.Users)` -- `From(x => x.dbo.Users.As(x.u))` -- `From(x => "dbo.Users AS u")` -- `From(x => x(subQuery).As("u"))` -- `Join(x => x.Left().Accounts.As(x.a).On(x.a.userId == x.u.id))` - -Coverage references: - -- `DynamORM.Tests/Select/ParserTests.cs` -- `DynamORM.Tests/Select/LegacyParserTests.cs` - -## Choosing Between Syntaxes - -Use dynamic table syntax when: - -- You want short CRUD calls quickly. -- Table/column selection is runtime-driven. - -Use fluent builder syntax when: - -- You want explicit SQL structure and deterministic command text. -- You need complex joins/subqueries/parser features. -- You prefer typed projections (`Execute()`, `ScalarAs()`). - -## Stored Procedure Deep Dive - -`db.Procedures` provides dynamic invocation of stored procedures. - -## Basic Calls - -```csharp -var r0 = db.Procedures.sp_Exp_Scalar(); -var r1 = db.Procedures.sp_Exp_Scalar(); -``` - -## Namespaced Procedure Names - -`TryGetMember` composes member segments before invocation: - -```csharp -var r = db.Procedures.dbo.reporting.sp_MonthlySales(); -``` - -Final procedure name becomes `dbo.reporting.sp_MonthlySales`. - -## Direction Prefixes - -Argument name prefixes drive parameter direction: - -- `out_` => `ParameterDirection.Output` -- `ret_` => `ParameterDirection.ReturnValue` -- `both_` => `ParameterDirection.InputOutput` - -Example: - -```csharp -dynamic res = db.Procedures.sp_Test_Scalar_In_Out( - inp: Guid.NewGuid(), - out_outp: Guid.Empty, - ret_Return: 0); -``` - -Returned object exposes values without prefixes. - -## Advanced Parameter Shape - -You can pass: - -- plain values -- `DynamicColumn` with schema and direction metadata -- `DynamicSchemaColumn` -- `DynamicExpando` / `ExpandoObject` - -Example with explicit output schema: - -```csharp -var res = db.Procedures.sp_Exp_SomeInputAndOutput( - Name: "G4g4r1n", - out_Result: new DynamicSchemaColumn { Name = "Result", Size = 256 }, - ret_Return: 0); -``` - -## Generic Result Shapes - -From `DynamicProcedureInvoker` behavior: - -- `T == IDataReader`: returns cached reader (`CachedReader()`). -- `T == DataTable`: materializes via `ToDataTable()`. -- `T == IEnumerable`: dynamic row enumeration. -- `T == IEnumerable`: scalar conversion per row. -- `T == IEnumerable`: mapping with mapper cache. -- `T == class`: mapped single/object payload. - -## Return and Output Aggregation - -When output/return params are present, DynamORM aggregates: - -- main result -- each output parameter -- return value - -into a dynamic object or mapped result type (if a mapping type is provided).