From 984870f51066f0d18a94923f071e9ab30334de14 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 07:49:08 +0100 Subject: [PATCH 1/8] Add full Markdown documentation set --- README.md | 23 +++++++ docs/README.md | 37 +++++++++++ docs/dynamic-table-api.md | 80 +++++++++++++++++++++++ docs/fluent-builder-api.md | 84 +++++++++++++++++++++++++ docs/mapping-and-entities.md | 101 ++++++++++++++++++++++++++++++ docs/net40-amalgamation.md | 42 +++++++++++++ docs/quick-start.md | 65 +++++++++++++++++++ docs/stored-procedures.md | 51 +++++++++++++++ docs/test-driven-examples.md | 70 +++++++++++++++++++++ docs/transactions-and-disposal.md | 65 +++++++++++++++++++ 10 files changed, 618 insertions(+) create mode 100644 README.md create mode 100644 docs/README.md create mode 100644 docs/dynamic-table-api.md create mode 100644 docs/fluent-builder-api.md create mode 100644 docs/mapping-and-entities.md create mode 100644 docs/net40-amalgamation.md create mode 100644 docs/quick-start.md create mode 100644 docs/stored-procedures.md create mode 100644 docs/test-driven-examples.md create mode 100644 docs/transactions-and-disposal.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc50393 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# DynamORM + +Dynamic ORM and SQL builder for .NET, with dynamic and typed APIs. + +## Documentation + +Full documentation is available in [`docs/README.md`](docs/README.md): + +- Quick start +- Dynamic table API +- Fluent builder API +- Mapping and entity lifecycle +- Transaction/disposal semantics +- Stored procedures +- .NET 4.0 amalgamation workflow +- Test-driven examples + +## Repository Layout + +- `DynamORM/`: core library +- `DynamORM.Tests/`: test suite +- `AmalgamationTool/`: amalgamation generator and generated single-file output +- `DynamORM.Net40.csproj`: net40 build for amalgamated source compatibility diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ce2fc63 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,37 @@ +# DynamORM Documentation + +This documentation is based on: +- XML comments in the library source (`DynamORM/*.cs`, `DynamORM/*/*.cs`). +- Executable usage patterns from the test suite (`DynamORM.Tests`). + +## Contents + +- [Quick Start](quick-start.md) +- [Dynamic Table API](dynamic-table-api.md) +- [Fluent Builder API](fluent-builder-api.md) +- [Mapping and Entities](mapping-and-entities.md) +- [Transactions and Disposal](transactions-and-disposal.md) +- [Stored Procedures](stored-procedures.md) +- [.NET 4.0 Amalgamation](net40-amalgamation.md) +- [Test-Driven Examples](test-driven-examples.md) + +## Supported Targets + +Main library targets are defined in `DynamORM/DynamORM.csproj`. + +For legacy .NET 4.0 consumers, use the amalgamated source workflow documented in [.NET 4.0 Amalgamation](net40-amalgamation.md). + +## Design Overview + +DynamORM provides two major usage styles: + +- Dynamic table access: + - `db.Table("users").Count(...)` + - `db.Table("users").Insert(...)` + - `db.Table("users").Query(...)` +- Fluent SQL builder access: + - `db.From()` + - `db.Select()` + - `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. diff --git a/docs/dynamic-table-api.md b/docs/dynamic-table-api.md new file mode 100644 index 0000000..61ec861 --- /dev/null +++ b/docs/dynamic-table-api.md @@ -0,0 +1,80 @@ +# Dynamic Table API + +The dynamic table API centers on `DynamicTable` and allows expressive calls like: + +```csharp +dynamic users = db.Table("users"); +``` + +## Read Operations + +Examples backed by `DynamORM.Tests/Select/DynamicAccessTests.cs`: + +```csharp +users.Count(columns: "id"); +users.First(columns: "id"); +users.Last(columns: "id"); +users.Min(columns: "id"); +users.Max(columns: "id"); +users.Avg(columns: "id"); +users.Sum(columns: "id"); +users.Scalar(columns: "first", id: 19); +users.Single(id: 19); +users.Query(columns: "first,last", order: "id:desc"); +``` + +## Conditions with `DynamicColumn` + +```csharp +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)); +``` + +## Insert + +```csharp +users.Insert(code: "201", first: "Juri", last: "Gagarin"); + +users.Insert(values: new +{ + code = "202", + first = "Juri", + last = "Gagarin" +}); +``` + +## Update + +```csharp +users.Update(id: 1, first: "Yuri", last: "Gagarin"); + +users.Update( + values: new { first = "Yuri" }, + where: new { id = 1 }); +``` + +## Delete + +```csharp +users.Delete(code: "201"); +users.Delete(where: new { id = 14, code = 14 }); +``` + +## Typed Dynamic Table Calls + +`DynamicTable` methods also accept `type: typeof(T)` for mapped class scenarios: + +```csharp +users.Count(type: typeof(User), columns: "id"); +users.Query(type: typeof(User)); +``` + +These usage patterns are covered in `DynamORM.Tests/Select/TypedAccessTests.cs`. + +## Notes + +- Dynamic member names map to table/column names and builder conventions. +- Unknown dynamic operations throw `InvalidOperationException`. +- Explicit schema behavior depends on `DynamicDatabaseOptions.SupportSchema`. diff --git a/docs/fluent-builder-api.md b/docs/fluent-builder-api.md new file mode 100644 index 0000000..57985d7 --- /dev/null +++ b/docs/fluent-builder-api.md @@ -0,0 +1,84 @@ +# Fluent Builder API + +The fluent API is built around interfaces like `IDynamicSelectQueryBuilder`, `IDynamicInsertQueryBuilder`, `IDynamicUpdateQueryBuilder`, and `IDynamicDeleteQueryBuilder`. + +## Core Select Flow + +```csharp +using (var query = db.From("users", "u") + .Where("u.id", 19) + .SelectColumn("u.first", "u.last")) +{ + var result = query.Execute().ToList(); +} +``` + +## Typed Select + +```csharp +using (var query = db.From("u") + .Where(x => x.u.id == 19) + .Select(x => x.u.All())) +{ + var users = query.Execute().ToList(); +} +``` + +## Parser Lambda Patterns + +The parser supports patterns tested in `DynamORM.Tests/Select/ParserTests.cs`: + +- `From(x => x.dbo.Users)` +- `From(x => x.dbo.Users.As(x.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))` +- `GroupBy(x => x.u.last)` +- `Having(x => x.Count() > 1)` +- `OrderBy(x => x.u.id.Desc())` + +## Subqueries + +```csharp +var sub = new DynamicSelectQueryBuilder(db).From(x => x.dbo.Users); + +using (var query = new DynamicSelectQueryBuilder(db) + .From(x => x(sub).As("u")) + .Select(x => x.u.All())) +{ + var rows = query.Execute().ToList(); +} +``` + +## Scalar Helpers + +```csharp +var count = db.From() + .Select(x => x.Count()) + .ScalarAs(); +``` + +## Modify Builders + +```csharp +db.Insert("users").Values("code", "301").Values("first", "Ada").Execute(); + +db.Update("users") + .Values("first", "Alicia") + .Where("id", 301) + .Execute(); + +db.Delete("users") + .Where("id", 301) + .Execute(); +``` + +## SQL Inspection + +You can inspect generated SQL from builder objects: + +```csharp +var sql = query.CommandText(); +``` + +Command text assertions are heavily used in parser tests. diff --git a/docs/mapping-and-entities.md b/docs/mapping-and-entities.md new file mode 100644 index 0000000..319617e --- /dev/null +++ b/docs/mapping-and-entities.md @@ -0,0 +1,101 @@ +# Mapping and Entities + +DynamORM supports attribute-driven mapping and entity lifecycle helpers. + +## Mapping Attributes + +## `TableAttribute` + +```csharp +[Table(Name = "users", Owner = "dbo", Override = true)] +public class User +{ + // ... +} +``` + +- `Name`: table name override. +- `Owner`: schema/owner segment. +- `Override`: prefer attribute schema info over provider schema. + +## `ColumnAttribute` + +```csharp +public class User +{ + [Column("id", isKey: true)] + public int Id { get; set; } + + [Column("email")] + public string Email { get; set; } +} +``` + +Important flags: + +- `IsKey` +- `AllowNull` +- `IsNoInsert` +- `IsNoUpdate` +- `Type`, `Size`, `Precision`, `Scale` + +## Ignore Fields + +Use `IgnoreAttribute` to skip properties in mapper-driven workflows. + +## Validation + +`RequiredAttribute` can define value rules. + +```csharp +public class Profile +{ + [Required(1f, 10f)] + public int Rank { get; set; } + + [Required(2, true)] + [Required(7, 18, ElementRequirement = true)] + public decimal[] Scores { get; set; } +} + +var issues = DynamicMapperCache.GetMapper().ValidateObject(profile); +``` + +Validation scenarios are verified in `DynamORM.Tests/Helpers/Validation/ObjectValidationTest.cs`. + +## `DynamicEntityBase` + +`DynamicEntityBase` tracks state and changed fields. + +```csharp +public class UserEntity : DynamicEntityBase +{ + [Column("id", true)] + public int Id { get; set; } + + [Column("first")] + public string First { get; set; } +} +``` + +Available behaviors: + +- `Validate()` +- `Insert(db)` +- `Update(db)` +- `Delete(db)` +- `Refresh(db)` +- `Save(db)` driven by `DynamicEntityState` + +## Repository Base + +`DynamicRepositoryBase` provides common operations: + +- `GetAll()` +- `GetByQuery(...)` +- `Insert(...)` +- `Update(...)` +- `Delete(...)` +- `Save(...)` + +It ensures query/table compatibility for `T` unless explicitly bypassed. diff --git a/docs/net40-amalgamation.md b/docs/net40-amalgamation.md new file mode 100644 index 0000000..f050cdc --- /dev/null +++ b/docs/net40-amalgamation.md @@ -0,0 +1,42 @@ +# .NET 4.0 Amalgamation + +Legacy .NET 4.0 consumers should use the amalgamated source model. + +## Files + +- `AmalgamationTool/DynamORM.Amalgamation.cs`: merged source file. +- `DynamORM.Net40.csproj`: net40 build project that compiles the amalgamation output. + +## Regenerate Amalgamation + +The amalgamation file must be regenerated after library source changes. + +Current workflow in this repository: + +1. Build or run `AmalgamationTool` (or use an equivalent merge script). +2. Merge all files from `DynamORM/` into `AmalgamationTool/DynamORM.Amalgamation.cs`. +3. Build `DynamORM.Net40.csproj`. + +## Build in Mono Container + +```bash +docker run --rm -v "$PWD":/src -w /src mono:latest \ + bash -lc "msbuild /t:Build /p:Configuration=Release DynamORM.Net40.csproj" +``` + +Expected output artifact: + +- `bin/Release/net40/DynamORM.Net40.dll` + +## Build with .NET SDK Container + +```bash +docker run --rm -v "$PWD":/src -w /src mcr.microsoft.com/dotnet/sdk:10.0-preview \ + dotnet build DynamORM.Net40.csproj -c Release +``` + +## Important Constraint + +Do not force net40 into the main multi-target `DynamORM/DynamORM.csproj` for modern workflows. + +Keep net40 compatibility validated through the dedicated amalgamation project so legacy consumers can embed the single-file source without destabilizing primary targets. diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..4a24707 --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,65 @@ +# Quick Start + +## Install and Reference + +Reference the `DynamORM` project or package from your application. + +## Basic Setup + +```csharp +using System.Data.SQLite; +using DynamORM; + +var options = + DynamicDatabaseOptions.SingleConnection | + DynamicDatabaseOptions.SingleTransaction | + DynamicDatabaseOptions.SupportLimitOffset | + DynamicDatabaseOptions.SupportSchema; + +using (var db = new DynamicDatabase( + SQLiteFactory.Instance, + "Data Source=app.db;", + options)) +{ + db.DumpCommands = true; + + var users = db.Table("users"); + var total = users.Count(columns: "id"); + var first = users.First(columns: "id,first,last"); +} +``` + +This setup mirrors `DynamORM.Tests/TestsBase.cs`. + +## First Query (Dynamic API) + +```csharp +using (var db = new DynamicDatabase(SQLiteFactory.Instance, "Data Source=app.db;", options)) +{ + var row = db.Table("users").Single(id: 19); + Console.WriteLine(row.first); +} +``` + +## First Query (Fluent Builder API) + +```csharp +using (var db = new DynamicDatabase(SQLiteFactory.Instance, "Data Source=app.db;", options)) +using (var query = db.From("users").Where("id", 19).SelectColumn("first")) +{ + var first = query.ScalarAs(); + Console.WriteLine(first); +} +``` + +## Insert, Update, Delete + +```csharp +var table = db.Table("users"); + +table.Insert(code: "201", first: "Juri", last: "Gagarin"); +table.Update(values: new { first = "Yuri" }, where: new { code = "201" }); +table.Delete(code: "201"); +``` + +These forms are validated in `DynamORM.Tests/Modify/DynamicModificationTests.cs`. diff --git a/docs/stored-procedures.md b/docs/stored-procedures.md new file mode 100644 index 0000000..0a5e09e --- /dev/null +++ b/docs/stored-procedures.md @@ -0,0 +1,51 @@ +# Stored Procedures + +Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled. + +## Basic Invocation + +```csharp +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 + +Dynamic member chaining builds qualified names: + +```csharp +var res = db.Procedures.dbo.reporting.MyProc(); +``` + +This resolves to `dbo.reporting.MyProc`. + +## Result Mapping + +If generic return types are provided, DynamORM attempts mapper-based projection into the target type. + +If output parameters are present, result payload is assembled from: + +- main scalar/resultset-derived value +- output values +- optional return value + +The behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs` and documented in XML examples in `DynamORM/DynamicDatabase.cs`. diff --git a/docs/test-driven-examples.md b/docs/test-driven-examples.md new file mode 100644 index 0000000..27daf66 --- /dev/null +++ b/docs/test-driven-examples.md @@ -0,0 +1,70 @@ +# Test-Driven Examples + +This page maps concrete examples to test files so behavior can be verified quickly. + +## Dynamic Select and Aggregates + +Source: `DynamORM.Tests/Select/DynamicAccessTests.cs` + +- `Count/Min/Max/Avg/Sum/Scalar` +- `First/Last/Single` +- `IN`, `LIKE`, `BETWEEN`, comparison operators +- ad-hoc column expressions and aliases + +## Typed Access and Mapping + +Source: `DynamORM.Tests/Select/TypedAccessTests.cs` + +- `Query(type: typeof(T))` +- `Execute()` +- projection with `MapEnumerable()` +- table mapping and strongly typed expressions + +## SQL Parser and CommandText Expectations + +Source: `DynamORM.Tests/Select/ParserTests.cs` + +- `From` variants (`schema.table`, aliasing, string forms) +- join expressions and join kind helpers +- subquery embedding +- `NoLock()` translation +- deterministic SQL text generation checks + +## Data Modification + +Source: `DynamORM.Tests/Modify/DynamicModificationTests.cs` + +- insert via named args, anonymous object, mapped class, plain class +- update via key/object/where +- delete via key/object/where + +## Schema Variants + +Sources: + +- `DynamORM.Tests/Modify/DynamicTypeSchemaModificationTests.cs` +- `DynamORM.Tests/Modify/DynamicNoSchemaModificationTests.cs` +- `DynamORM.Tests/Select/DynamicTypeSchemaAccessTests.cs` +- `DynamORM.Tests/Select/DynamicNoSchemaAccessTests.cs` + +Use these to validate behavior with and without schema support. + +## Resource and Pooling Semantics + +Source: `DynamORM.Tests/Helpers/PoolingTests.cs` + +- command invalidation after database disposal +- transaction rollback behavior during disposal + +## Validation + +Source: `DynamORM.Tests/Helpers/Validation/ObjectValidationTest.cs` + +- rule-based object validation with `RequiredAttribute` +- array element-level validation + +## Dynamic Parser (Helper Layer) + +Source: `DynamORM.Tests/Helpers/Dynamic/DynamicParserTests.cs` + +Covers lower-level parser behavior used by fluent lambda parsing. diff --git a/docs/transactions-and-disposal.md b/docs/transactions-and-disposal.md new file mode 100644 index 0000000..694b835 --- /dev/null +++ b/docs/transactions-and-disposal.md @@ -0,0 +1,65 @@ +# Transactions and Disposal + +DynamORM manages connections, command pools, and transaction stacks internally. + +## Connection and Transaction Options + +`DynamicDatabaseOptions` controls behavior: + +- `SingleConnection` +- `SingleTransaction` +- `SupportSchema` +- `SupportStoredProcedures` +- `SupportNoLock` +- `SupportTop` / `SupportLimitOffset` / `SupportFirstSkip` + +Typical setup: + +```csharp +var options = + DynamicDatabaseOptions.SingleConnection | + DynamicDatabaseOptions.SingleTransaction | + DynamicDatabaseOptions.SupportLimitOffset | + DynamicDatabaseOptions.SupportSchema; +``` + +## Transaction Usage + +```csharp +using (var db = new DynamicDatabase(factory, connectionString, options)) +using (var conn = db.Open()) +using (var tx = conn.BeginTransaction()) +using (var cmd = conn.CreateCommand()) +{ + cmd.SetCommand("UPDATE users SET first = 'Ada' WHERE id = 1").ExecuteNonQuery(); + tx.Commit(); +} +``` + +Global transaction mode is also available via `db.BeginTransaction()`. + +## Disposal Guarantees + +Current disposal behavior includes idempotent guards on key objects: + +- `DynamicDatabase` +- `DynamicConnection` +- `DynamicCommand` +- `DynamicTransaction` +- `DynamicTable` +- builder and parser helpers + +This prevents repeated cleanup from throwing or re-disposing lower-level resources. + +## Pooling and Rollback Behavior + +Behavior validated by `DynamORM.Tests/Helpers/PoolingTests.cs`: + +- Disposing the database invalidates active commands. +- Open transactions are rolled back during disposal when not committed. + +## Practices + +- Prefer `using` blocks for `DynamicDatabase`, connections, commands, transactions, and builders. +- Do not manually re-dispose the same object from multiple ownership paths unless `IsDisposed` is checked. +- Keep transaction scope short and explicit. From 2c25e78befb4b026da2829310456e4a6630f7020 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 07:57:45 +0100 Subject: [PATCH 2/8] Expand docs with syntax/procedure deep dive and ADO.NET extensions --- README.md | 2 + docs/README.md | 2 + docs/ado-net-extensions.md | 163 ++++++++++++++++++++++ docs/stored-procedures.md | 2 + docs/syntax-and-procedures-deep-dive.md | 173 ++++++++++++++++++++++++ 5 files changed, 342 insertions(+) create mode 100644 docs/ado-net-extensions.md create mode 100644 docs/syntax-and-procedures-deep-dive.md diff --git a/README.md b/README.md index cc50393..b1def78 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ 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 +- ADO.NET extensions and cached reader - .NET 4.0 amalgamation workflow - Test-driven examples diff --git a/docs/README.md b/docs/README.md index ce2fc63..d5b2993 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,9 +9,11 @@ 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) +- [ADO.NET Extensions](ado-net-extensions.md) - [.NET 4.0 Amalgamation](net40-amalgamation.md) - [Test-Driven Examples](test-driven-examples.md) diff --git a/docs/ado-net-extensions.md b/docs/ado-net-extensions.md new file mode 100644 index 0000000..ff8cc52 --- /dev/null +++ b/docs/ado-net-extensions.md @@ -0,0 +1,163 @@ +# ADO.NET Extensions Reference + +DynamORM exposes extension helpers mainly in: + +- `DynamORM/DynamicExtensions.cs` +- `DynamORM/Helpers/DataReaderExtensions.cs` +- `DynamORM/Helpers/ReaderExtensions.cs` + +## `IDbCommand` Extensions + +## Connection and Transaction + +- `SetConnection(this IDbCommand, IDbConnection)` +- `SetTransaction(this IDbCommand, IDbTransaction)` + +These enable fluent command setup. + +## `SetCommand` Overloads + +Core overloads: + +- `SetCommand(CommandType, int timeout, string text, params object[] args)` +- `SetCommand(int timeout, string text, params object[] args)` +- `SetCommand(CommandType, string text, params object[] args)` +- `SetCommand(string text, params object[] args)` +- `SetCommand(IDynamicQueryBuilder builder)` + +Examples: + +```csharp +cmd.SetCommand("SELECT * FROM users WHERE id = {0}", 19); +cmd.SetCommand(CommandType.StoredProcedure, "sp_DoWork"); +cmd.SetCommand(builder); +``` + +## Parameter Helpers + +Bulk: + +- `AddParameters(DynamicDatabase, params object[] args)` +- `AddParameters(DynamicDatabase, ExpandoObject)` +- `AddParameters(DynamicDatabase, DynamicExpando)` + +Single parameter helpers: + +- `AddParameter(DynamicDatabase, object item)` +- `AddParameter(DynamicDatabase, string name, object item)` +- `AddParameter(IDynamicQueryBuilder, DynamicSchemaColumn? col, object value)` +- `AddParameter(IDynamicQueryBuilder, DynamicColumn item)` + +Advanced overloads support explicit `ParameterDirection`, `DbType`, `size`, `precision`, `scale`. + +Example: + +```csharp +cmd.AddParameter("@Result", ParameterDirection.Output, DbType.String, 256, 0, 0, DBNull.Value); +cmd.AddParameter("@Name", DbType.String, 50, "Alice"); +``` + +## Updating Existing Parameters + +- `SetParameter(this IDbCommand, string parameterName, object value)` +- `SetParameter(this IDbCommand, int index, object value)` + +## Execution and Conversion + +- `ExecuteScalarAs()` +- `ExecuteScalarAs(defaultValue)` +- `ExecuteScalarAs(TryParseHandler)` +- `ExecuteScalarAs(defaultValue, TryParseHandler)` +- `ExecuteEnumeratorOf(defaultValue, TryParseHandler)` + +These convert ADO.NET results to requested types with fallback behavior. + +## Command Debugging + +- `DumpToString()` +- `Dump(StringBuilder)` +- `Dump(TextWriter)` + +Useful with `DynamicDatabase.DumpCommands` and custom log sinks. + +## `IDataReader` and Row Helpers + +From `DynamicExtensions.cs`: + +- `ToList(this IDataReader)` +- `RowToDynamic(this IDataReader)` +- `RowToExpando(this IDataReader)` +- `RowToDynamic(this DataRow)` +- `RowToExpando(this DataRow)` +- upper-case variants (`RowToDynamicUpper`, `RowToExpandoUpper`) +- `GetFieldDbType(this IDataReader, int i)` + +## Reader Caching + +- `CachedReader(this IDataReader, int offset = 0, int limit = -1, Func progress = null)` + +This creates an in-memory `DynamicCachedReader` snapshot. + +## `DataReaderExtensions` + +- `ToDataTable(this IDataReader, string name = null, string nameSpace = null)` + +Converts current reader rows/schema to a `DataTable`. + +## `ReaderExtensions` Null-Safe Accessors + +Typed nullable access by column name: + +- `GetBooleanIfNotNull` +- `GetByteIfNotNull` +- `GetCharIfNotNull` +- `GetDateTimeIfNotNull` +- `GetDecimalIfNotNull` +- `GetDoubleIfNotNull` +- `GetFloatIfNotNull` +- `GetGuidIfNotNull` +- `GetInt16IfNotNull` +- `GetInt32IfNotNull` +- `GetInt64IfNotNull` +- `GetStringIfNotNull` +- `GetValueIfNotNull` + +Each method accepts an optional default value and returns it when DB value is null. + +## `DynamicCachedReader` + +`DynamicCachedReader` implements `IDataReader` and stores: + +- schema metadata +- field names/types/ordinals +- full row cache (supports multi-result sets) + +Construction options: + +- `new DynamicCachedReader(IDataReader, offset, limit, progress)` +- `DynamicCachedReader.FromDynamicEnumerable(...)` +- `DynamicCachedReader.FromEnumerable(...)` +- `DynamicCachedReader.FromEnumerable(Type, IEnumerable)` + +Typical usage: + +```csharp +using (var rdr = cmd.ExecuteReader()) +using (var cached = rdr.CachedReader()) +{ + while (cached.Read()) + { + var row = cached.RowToDynamic(); + } +} +``` + +When to use it: + +- You need disconnected reader semantics. +- You need multiple passes or deferred mapping. +- You need stable materialization before connection disposal. + +Tradeoff: + +- Higher memory use proportional to result size. diff --git a/docs/stored-procedures.md b/docs/stored-procedures.md index 0a5e09e..05c1ba4 100644 --- a/docs/stored-procedures.md +++ b/docs/stored-procedures.md @@ -2,6 +2,8 @@ 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 diff --git a/docs/syntax-and-procedures-deep-dive.md b/docs/syntax-and-procedures-deep-dive.md new file mode 100644 index 0000000..e377b9c --- /dev/null +++ b/docs/syntax-and-procedures-deep-dive.md @@ -0,0 +1,173 @@ +# 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). From b628348af884ae262478d8f84ab7c585c10e8f3d Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 08:06:56 +0100 Subject: [PATCH 3/8] 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). From 6a219fc194b0c2c9e2a66b067c8f25c37b52c1b7 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 08:09:29 +0100 Subject: [PATCH 4/8] Add generic real-world extension method samples --- docs/ado-net-extensions.md | 159 +++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/docs/ado-net-extensions.md b/docs/ado-net-extensions.md index ff8cc52..258e786 100644 --- a/docs/ado-net-extensions.md +++ b/docs/ado-net-extensions.md @@ -161,3 +161,162 @@ When to use it: Tradeoff: - Higher memory use proportional to result size. + +## End-to-End Samples (Genericized) + +These samples mirror real production patterns while using generic names. + +## Sample 1: Stored Procedure with Multiple Result Sets + Null-Safe Reader Accessors + +```csharp +var records = new Dictionary(); + +using var con = Database.Open(); +using var cmd = con.CreateCommand() + .SetCommand(CommandType.StoredProcedure, "usp_get_messages"); + +using var r = cmd.ExecuteReader(); + +while (r.Read()) +{ + var msg = new GenericMessage + { + Id = r.GetStringIfNotNull("id"), + Type = r.GetStringIfNotNull("type"), + Value = r.GetStringIfNotNull("value"), + State = (GenericMessageState)(r.GetInt32IfNotNull("state") ?? 0), + Date = r.GetDateTimeIfNotNull("date") ?? new DateTime(2099, 12, 31), + }; + + records.Add(msg.Id, msg); +} + +if (r.NextResult()) +{ + while (r.Read()) + { + var messageId = r.GetStringIfNotNull("message_id"); + if (!records.ContainsKey(messageId)) + continue; + + records[messageId].Items.Add(new GenericMessageItem + { + Id = r.GetStringIfNotNull("id"), + Type = r.GetStringIfNotNull("type"), + Value = r.GetStringIfNotNull("value"), + }); + } +} +``` + +Extension methods used: + +- `SetCommand(CommandType, string)` +- `GetStringIfNotNull` +- `GetInt32IfNotNull` +- `GetDateTimeIfNotNull` + +## Sample 2: Stored Procedure with Input/Output Parameters + Result Check + +```csharp +using var con = Database.Open(); +using var cmd = con.CreateCommand() + .SetCommand(CommandType.StoredProcedure, "usp_set_message_status") + .AddParameter(Database.GetParameterName("id"), DbType.String, (object)messageId) + .AddParameter(Database.GetParameterName("status"), DbType.Int32, (object)(int)status) + .AddParameter(Database.GetParameterName("note"), DbType.String, (object)note) + .AddParameter(Database.GetParameterName("result"), ParameterDirection.Output, DbType.Int32, 0, null) + .AddParameter(Database.GetParameterName("result_description"), ParameterDirection.Output, DbType.String, 1024, null); + +cmd.ExecuteNonQuery(); + +var result = cmd.Parameters[Database.GetParameterName("result")] as IDataParameter; +var resultDescription = cmd.Parameters[Database.GetParameterName("result_description")] as IDataParameter; + +if (result != null && result.Value != DBNull.Value && result.Value != null) +{ + if ((int)result.Value != 0) + { + var description = (resultDescription != null && + resultDescription.Value != DBNull.Value && + resultDescription.Value != null) + ? resultDescription.Value.ToString() + : "UNSPECIFIED"; + + throw new Exception($"{result.Value} - {description}"); + } +} +``` + +Extension methods used: + +- `SetCommand(CommandType, string)` +- `AddParameter(...)` overloads with `DbType` and `ParameterDirection` +- `DumpToString()` (optional diagnostic logging) + +## Sample 3: Parameterized `UPDATE` with Runtime SQL and ADO.NET Extensions + +```csharp +using var conn = Database.Open(); + +using var cmdUpdate = conn.CreateCommand() + .SetCommand($@"UPDATE {Database.DecorateName("transport_task")} +SET {Database.DecorateName("confirmed")} = {Database.GetParameterName("confirmed")} +WHERE {Database.DecorateName("id")} = {Database.GetParameterName("id")}") + .AddParameter(Database.GetParameterName("confirmed"), DbType.Int32, (object)1) + .AddParameter(Database.GetParameterName("id"), DbType.Guid, (object)taskId); + +if (cmdUpdate.ExecuteNonQuery() > 0) +{ + // updated +} +``` + +Extension methods used: + +- `SetCommand(string, params object[])` +- `AddParameter(string, DbType, object)` + +## Sample 4: Parameterized `SELECT` + Reader Mapping Method + +```csharp +using var conn = Database.Open(); + +using var cmd = conn.CreateCommand() + .SetCommand($@"SELECT * FROM {Database.DecorateName("transport_task")} +WHERE {Database.DecorateName("id")} = {Database.GetParameterName("id")}") + .AddParameter(Database.GetParameterName("id"), DbType.Guid, (object)id); + +using var r = cmd.ExecuteReader(); +if (r.Read()) + return ReadTask(r); + +return null; + +protected virtual MoveTask ReadTask(IDataReader r) +{ + var source = r.GetStringIfNotNull("Source"); + var destination = r.GetStringIfNotNull("Destination"); + + return new MoveTask + { + Id = r.GetGuid(r.GetOrdinal("Id")), + Created = r.GetDateTime(r.GetOrdinal("Created")), + StartDate = r.GetDateTimeIfNotNull("StartDate"), + EndDate = r.GetDateTimeIfNotNull("EndDate"), + ExternalId = r.GetStringIfNotNull("ExternalId"), + TaskType = (TransportTaskType)(r.GetInt32IfNotNull("Type") ?? 0), + Source = string.IsNullOrEmpty(source) ? null : Deserialize
(source), + Destination = string.IsNullOrEmpty(destination) ? null : Deserialize
(destination), + Priority = r.GetInt32IfNotNull("Priority") ?? 0, + }; +} +``` + +Extension methods used: + +- `SetCommand(...)` +- `AddParameter(...)` +- `GetStringIfNotNull` +- `GetDateTimeIfNotNull` +- `GetInt32IfNotNull` From c1a35eec7ad4142b67003205ac1961289eaddfb3 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 08:13:43 +0100 Subject: [PATCH 5/8] Document AddParameter overload resolution best practice --- docs/ado-net-extensions.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/ado-net-extensions.md b/docs/ado-net-extensions.md index 258e786..f3686cc 100644 --- a/docs/ado-net-extensions.md +++ b/docs/ado-net-extensions.md @@ -50,6 +50,13 @@ Single parameter helpers: Advanced overloads support explicit `ParameterDirection`, `DbType`, `size`, `precision`, `scale`. +Compatibility note on overload resolution: + +- Best practice is to cast value arguments to `object` when calling `AddParameter(...)` with value payloads. +- Alternatively, use named arguments to force the intended overload. +- This avoids accidental binding to a different overload in ambiguous cases. +- Current overload shape is preserved for backward compatibility. + Example: ```csharp From cf15a898be57f2adabe484606dc7c9a311ebe961 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 08:19:38 +0100 Subject: [PATCH 6/8] Expand fluent syntax docs with parser and join nuances --- docs/fluent-builder-api.md | 325 ++++++++++++++++++++++++++----------- 1 file changed, 233 insertions(+), 92 deletions(-) diff --git a/docs/fluent-builder-api.md b/docs/fluent-builder-api.md index 49c8e82..55449eb 100644 --- a/docs/fluent-builder-api.md +++ b/docs/fluent-builder-api.md @@ -1,122 +1,263 @@ # Fluent Builder API -The fluent API is built around interfaces like: +The fluent API is built around: - `IDynamicSelectQueryBuilder` - `IDynamicInsertQueryBuilder` - `IDynamicUpdateQueryBuilder` - `IDynamicDeleteQueryBuilder` -This API is best when SQL structure must be explicit and composable. +This page documents call semantics and parser nuances from: -## Core Select Flow +- `DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs` +- `DynamORM/Builders/Implementation/DynamicQueryBuilder.cs` +- `DynamORM/Builders/Extensions/DynamicWhereQueryExtensions.cs` +- `DynamORM/Builders/Extensions/DynamicHavingQueryExtensions.cs` +- parser tests in `DynamORM.Tests/Select/ParserTests.cs` and `LegacyParserTests.cs` + +## Builder Lifecycle and Clause Output Order + +Builder methods can be chained in many orders, but generated SQL is emitted in canonical order: + +1. `SELECT` +2. `FROM` +3. `JOIN` +4. `WHERE` +5. `GROUP BY` +6. `HAVING` +7. `ORDER BY` +8. paging (`TOP`/`FIRST SKIP` or `LIMIT OFFSET` depending on options) + +Recommendation: start with `From(...)` first so alias registration is unambiguous for later clauses. + +## Fast Baseline ```csharp using (var query = db.From("users", "u") .Where("u.id", 19) .SelectColumn("u.first", "u.last")) -{ - var result = query.Execute().ToList(); -} -``` - -## Typed Select - -```csharp -using (var query = db.From("u") - .Where(x => x.u.id == 19) - .Select(x => x.u.All())) -{ - var users = query.Execute().ToList(); -} -``` - -## Parser Lambda Patterns - -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))` -- `GroupBy(x => x.u.last)` -- `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(); } ``` +## `From(...)` Deep Dive + +Supported source forms: + +- string source: + - `From(x => "dbo.Users")` + - `From(x => "dbo.Users AS u")` +- member source: + - `From(x => x.dbo.Users)` + - `From(x => x.dbo.Users.As(x.u))` +- typed source: + - `From(x => x(typeof(User)).As(x.u))` + - `db.From("u")` +- subquery source: + - `From(x => x(subQuery).As(x.u))` + +Aliasing nuance: + +- `As(...)` is optional for simple table/member source. +- For generic invoke source (`x => x(expression)`), aliasing is effectively required when the source should be referenced later. + +`NoLock()` nuance: + +- `NoLock()` is parsed in `From`/`Join`, but SQL `WITH(NOLOCK)` is emitted only if database option `SupportNoLock` is enabled. + +Examples from tests: + +```csharp +cmd.From(x => x.dbo.Users.As(x.u)); +cmd.From(x => x("\"dbo\".\"Users\"").As(x.u)); +cmd.From(x => x(cmd.SubQuery().From(y => y.dbo.Users)).As(x.u)); +cmd.From(x => x.dbo.Users.As("u").NoLock()); +``` + +## `Join(...)` Deep Dive + +`Join(...)` supports table/member/string/invoke/subquery forms with `On(...)` conditions. + +### Two-pass join processing + +Implementation runs join parsing in two passes: + +- pass 1: collect tables/aliases +- pass 2: render SQL conditions + +This enables robust alias resolution inside `On(...)` expressions. + +### Join type resolution + +Join type comes from a dynamic method near the root of the join expression. + +Common forms: + +- `Inner()` -> `INNER JOIN` +- `Left()` -> `LEFT JOIN` +- `LeftOuter()` -> `LEFT OUTER JOIN` +- `Right()` -> `RIGHT JOIN` +- `RightOuter()` -> `RIGHT OUTER JOIN` +- no type method -> plain `JOIN` + +Non-obvious option: + +- the join-type method can accept arguments. +- if first argument is `false`, auto-append/split of `JOIN` is disabled. +- if any argument is a string, that string is used as explicit join type text. + +This allows custom forms such as provider-specific joins. + +Example patterns: + +```csharp +cmd.Join(x => x.Inner().dbo.UserClients.As(x.uc).On(x.u.Id == x.uc.UserId)); + +cmd.Join(x => x.Custom(false, "CROSS APPLY") + .dbo.UserClients.As(x.uc) + .On(x.u.Id == x.uc.UserId)); +``` + +### `On(...)` ordering rule + +`On(...)` can appear with other join modifiers in the same lambda chain. Parsed order is normalized by node traversal. + +Recommended readable order: + +```csharp +.Join(x => x.Inner().dbo.UserClients.As(x.uc).NoLock().On(x.u.Id == x.uc.UserId)) +``` + +Validated join variants are heavily covered in parser tests (`TestJoinClassic`, `TestInnerJoin*`, `TestLeftJoin`, `TestRightJoin`, `TestSubQueryJoin`). + +## `Where(...)` and `Having(...)` Nuances + +Both clauses share the same behavior model. + +### Lambda conditions + +```csharp +.Where(x => x.u.UserName == "admin") +.Where(x => x.Or(x.u.IsActive == true)) +``` + +Chaining behavior: + +- each chained call defaults to `AND` +- root wrapper `Or(...)` switches that clause append to `OR` + +### `DynamicColumn` behavior + +`DynamicColumn` supports: + +- comparison operators (`Eq`, `Not`, `Like`, `NotLike`, `In`, `Between`, etc.) +- `SetOr()` to force OR join with previous condition +- `SetBeginBlock()` / `SetEndBlock()` to control parenthesis grouping +- `SetVirtualColumn(...)` to influence null parameterization behavior + +Example (legacy parser tests): + +```csharp +.Where(new DynamicColumn("u.Deleted").Eq(0).SetBeginBlock()) +.Where(new DynamicColumn("u.IsActive").Eq(1).SetOr().SetEndBlock()) +``` + +### Object conditions + `_table` + +```csharp +.Where(new { Deleted = 0, IsActive = 1, _table = "u" }) +``` + +Nuance: + +- `_table` applies alias/table prefix during object-condition expansion. +- when `schema: true`, only key-like mapped columns are included. + +`Having(...)` mirrors these forms and semantics. + +## `Select(...)` Nuances + +Supported forms: + +- simple member: `Select(x => x.u.UserName)` +- aliasing: `Select(x => x.u.UserName.As(x.Name))` +- all columns: `Select(x => x.u.All())` +- aggregate functions: `Select(x => x.Sum(x.u.Score).As(x.Total))` +- anonymous projection: `Select(x => new { Id = x.u.Id, Name = x.u.UserName })` +- raw invoke concatenation: `Select(x => x("CASE WHEN ", x.u.Active == 1, " THEN ", 1, " ELSE ", 0, " END").As(x.Flag))` + +Non-obvious behavior: + +- anonymous projection property names become SQL aliases. +- `All()` cannot be combined with alias emission for that same select item. +- invoke syntax (`x("...", expr, "...")`) is the parser escape hatch for complex SQL expressions. + +## `OrderBy(...)` Nuances + +Default direction is ascending. + +```csharp +.OrderBy(x => x.u.UserName) // ASC +.OrderBy(x => x.Desc(x.u.UserName)) // DESC +.OrderBy(x => "1 DESC") // numeric column position +.OrderBy(x => x.Desc(1)) // numeric column position +``` + +`OrderByColumn(...)` also supports `DynamicColumn` parsing style, including numeric alias position patterns from legacy tests. + +## `GroupBy(...)` + +```csharp +.GroupBy(x => x.u.UserName) +// or +.GroupByColumn("u.Name") +``` + +Group-by supports multiple entries by repeated calls or params arrays. + +## Paging and Distinct + +- `Distinct(true)` toggles `SELECT DISTINCT`. +- `Top(n)` delegates to `Limit(n)`. +- `Limit(n)` and `Offset(n)` validate database capability flags. + +Capability checks: + +- `Limit` requires one of: `SupportLimitOffset`, `SupportFirstSkip`, `SupportTop`. +- `Offset` requires one of: `SupportLimitOffset`, `SupportFirstSkip`. + +Unsupported combinations throw `NotSupportedException`. + ## Subqueries -```csharp -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")) - .Select(x => x.u.All())) -{ - var rows = query.Execute().ToList(); -} -``` - -## Scalar Helpers +Subqueries can be embedded in select/where/join contexts. ```csharp -var count = db.From() - .Select(x => x.Count()) - .ScalarAs(); +cmd.From(x => x.dbo.Users.As(x.u)) + .Select(x => x(cmd.SubQuery() + .From(y => y.dbo.AccessRights.As(y.a)) + .Where(y => y.a.User_Id == y.u.Id) + .Select(y => y.a.IsAdmin)).As(x.IsAdmin)); ``` -## Modify Builders +Supported tested placements: + +- `SELECT (subquery) AS alias` +- `WHERE column = (subquery)` +- `WHERE column IN (subquery)` +- `JOIN (subquery) AS alias ON ...` + +## Typed Execution and Scalar ```csharp -db.Insert("users") - .Values("code", "301") - .Values("first", "Ada") - .Execute(); - -db.Update("users") - .Values("first", "Alicia") - .Where("id", 301) - .Execute(); - -db.Delete("users") - .Where("id", 301) - .Execute(); +var list = db.From("u").Where(x => x.u.Active == true).Execute().ToList(); +var count = db.From("u").Select(x => x.Count()).ScalarAs(); ``` -Typed variant: +## Practical Patterns -```csharp -db.Insert() - .Values(x => x.code, "302") - .Values(x => x.first, "Grace") - .Execute(); -``` - -## SQL Inspection - -You can inspect generated SQL from builder objects: - -```csharp -var sql = query.CommandText(); -``` - -Command text assertions are heavily used in parser tests. +- Register source aliases early (`From(...)` first). +- Keep join lambdas readable with explicit `Inner/Left/Right` and `On(...)` at the end. +- Use invoke syntax only for cases the parser cannot express directly. +- Use legacy `DynamicColumn` API when you need explicit grouping flags (`SetBeginBlock`, `SetEndBlock`, `SetOr`). From e95bbd0f67067a76720d0fad19d2b755595edb39 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 08:30:43 +0100 Subject: [PATCH 7/8] Expand stored procedure docs with result-shape and output parameter guidance --- docs/stored-procedures.md | 242 ++++++++++++++++++++++++++++++-------- 1 file changed, 191 insertions(+), 51 deletions(-) diff --git a/docs/stored-procedures.md b/docs/stored-procedures.md index 4ddeabd..4b6820d 100644 --- a/docs/stored-procedures.md +++ b/docs/stored-procedures.md @@ -2,99 +2,239 @@ Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled. -## Basic Invocation +This page documents actual runtime behavior from `DynamicProcedureInvoker`. + +## Invocation Basics ```csharp var scalar = db.Procedures.sp_Exp_Scalar(); var scalarTyped = db.Procedures.sp_Exp_Scalar(); ``` -## Schema-Qualified Invocation - -Dynamic member chaining builds qualified names: +Schema-qualified naming is built through dynamic chaining: ```csharp var res = db.Procedures.dbo.reporting.MyProc(); ``` -This resolves to `dbo.reporting.MyProc`. +Final command name is `dbo.reporting.MyProc`. -## Input, Output, Return, InputOutput Parameters +## Parameter Direction Prefixes -Prefixes in argument names control parameter direction: +Because dynamic `out/ref` is limited, parameter direction is encoded by argument name prefix: -- `out_` for output -- `ret_` for return value -- `both_` for input/output +- `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); +dynamic res = db.Procedures.sp_Message_SetState( + id: "abc-001", + both_state: "pending", + out_message: "", + ret_code: 0); ``` -## Using `DynamicSchemaColumn` for Explicit Output Shape +Prefix is removed from the exposed output key (`out_message` -> `message`). + +## How to Specify Type/Length for Out Parameters + +This is the most common pain point. You have 2 primary options. + +## Option A: `DynamicSchemaColumn` (recommended for output-only) + +Use this when you need explicit type, length, precision, or scale. ```csharp -var res = db.Procedures.sp_Exp_SomeInputAndOutput( - Name: "G4g4r1n", - out_Result: new DynamicSchemaColumn +dynamic res = db.Procedures.sp_Message_SetState( + id: "abc-001", + out_message: new DynamicSchemaColumn { - Name = "Result", - Size = 256 + Name = "message", + Type = DbType.String, + Size = 1024, }, - ret_Return: 0); + ret_code: 0); ``` -## Using `DynamicColumn` for Direction + Value + Schema +## Option B: `DynamicColumn` (recommended for input/output with value) + +Use this when you need direction + value + schema in one object. ```csharp -var res = db.Procedures.sp_WithInputOutput( - both_State: new DynamicColumn +dynamic res = db.Procedures.sp_Message_SetState( + both_state: new DynamicColumn { - ColumnName = "State", + ColumnName = "state", ParameterDirection = ParameterDirection.InputOutput, Value = "pending", - Schema = new DynamicSchemaColumn { Name = "State", Size = 32 } + Schema = new DynamicSchemaColumn + { + Name = "state", + Type = DbType.String, + Size = 32, + } }); ``` -## Result Shapes +## Option C: Plain value + name prefix -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: +Quickest form, but type/size inference is automatic. ```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(); +dynamic res = db.Procedures.sp_Message_SetState( + id: "abc-001", + out_message: "", + ret_code: 0); ``` -## Output and Return Value Aggregation +Use Option A/B whenever output size/type must be controlled. -When output and/or return values are used, DynamORM aggregates: +## Return Shape Matrix (What You Actually Get) -- main result -- output parameters -- return value +`DynamicProcedureInvoker` chooses result shape from generic type arguments. -into a dynamic object or mapped class (if a target type is provided). +## No generic type arguments (`db.Procedures.MyProc(...)`) + +Main result path: + +- procedure executes via `ExecuteNonQuery()` +- if return-value support is enabled and return param is present, that value replaces affected-row count + +Final returned object: + +- if no out/ret params were requested: scalar main result (`int` or return value) +- if any out/ret/both params were requested: dynamic object with keys + +## One generic type argument (`MyProc(...)`) + +Main result type resolution: + +- `TMain == IDataReader` => returns cached reader (`DynamicCachedReader`) +- `TMain == DataTable` => returns `DataTable` +- `TMain == List` or `IEnumerable` => list of dynamic rows +- `TMain == List` => converted first-column list +- `TMain == List` => mapped list +- `TMain == primitive/string` => scalar converted value +- `TMain == complex class` => first row mapped to class (or `null`) + +Final returned object: + +- if no out/ret params were requested: `TMain` result directly +- if out/ret params exist: dynamic object containing main result + out params + +Important nuance: + +- when out params exist and only one generic type is provided, the result is a dynamic object, not bare `TMain`. + +## Two generic type arguments (`MyProc(...)`) + +This is the preferred pattern when out params are involved. + +- `TMain` is resolved as above +- out/main values are packed into a dictionary-like dynamic payload +- if mapper for `TOutModel` exists, payload is mapped to `TOutModel` +- otherwise fallback is dynamic object + +## Result Key Names in Out Payload + +When out payload is used: + +- main result is stored under procedure method name key (`binder.Name`) +- each out/both/ret param is stored under normalized parameter name (without prefix) + +Example call: + +```csharp +var res = db.Procedures.sp_Message_SetState( + id: "abc-001", + out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 }, + ret_code: 0); +``` + +Expected payload keys before mapping: + +- `sp_Message_SetState` (main result) +- `message` +- `code` + +## Preparing Result Classes Correctly + +Use `ColumnAttribute` to map returned payload keys. + +```csharp +public class MessageSetStateResult +{ + [Column("sp_Message_SetState")] + public int MainResult { get; set; } + + [Column("message")] + public string Message { get; set; } + + [Column("code")] + public int ReturnCode { get; set; } +} +``` + +If key names and property names already match, attributes are optional. + +## Common Patterns + +## Pattern 1: Main scalar only + +```csharp +int count = db.Procedures.sp_CountMessages(); +``` + +## Pattern 2: Output params + mapped output model + +```csharp +var res = db.Procedures.sp_Message_SetState( + id: "abc-001", + status: 2, + out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 }, + ret_code: 0); +``` + +## Pattern 3: Procedure returning dataset as table + +```csharp +DataTable dt = db.Procedures.sp_Message_GetBatch(batchId: 10); +``` + +## Pattern 4: Procedure returning mapped collection + +```csharp +List rows = db.Procedures.sp_Message_List>(status: 1); +``` + +## Pattern 5: Read output dynamically + +```csharp +dynamic res = db.Procedures.sp_Message_SetState( + id: "abc-001", + out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 }, + ret_code: 0); + +var message = (string)res.message; +var code = (int)res.code; +``` + +## Troubleshooting Checklist + +- Out value is truncated or null: + - define output schema explicitly with `DynamicSchemaColumn` (type + size) +- Unexpected return object shape: + - check whether any out/ret/both parameter was passed + - if yes, expect out payload object unless you used 2-generic mapping variant +- Mapping to class fails silently (dynamic fallback): + - ensure output model is mappable and keys match columns/properties +- Return value not appearing: + - ensure `ret_` parameter is supplied, or enable provider option `SupportStoredProceduresResult` ## 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`. +- Behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs`. +- XML examples also appear in `DynamORM/DynamicDatabase.cs`. From cd51c45e4bad4933e37391c21165b1ec321301b4 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 08:53:02 +0100 Subject: [PATCH 8/8] Clarify SupportStoredProceduresResult behavior across providers --- docs/stored-procedures.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/stored-procedures.md b/docs/stored-procedures.md index 4b6820d..c4fd6d0 100644 --- a/docs/stored-procedures.md +++ b/docs/stored-procedures.md @@ -4,6 +4,26 @@ Stored procedure support is available through `db.Procedures` when `DynamicDatab This page documents actual runtime behavior from `DynamicProcedureInvoker`. +## `SupportStoredProceduresResult` and Provider Differences + +`DynamicProcedureInvoker` can treat the procedure "main result" as either: + +- affected rows from `ExecuteNonQuery()` +- provider return value parameter + +This behavior is controlled by `DynamicDatabaseOptions.SupportStoredProceduresResult`. + +- `true`: if a return-value parameter is present, invoker uses that value as main result +- `false`: invoker keeps `ExecuteNonQuery()` result (safer for providers that do not expose SQL Server-like return value behavior) + +Why this matters: + +- SQL Server commonly supports procedure return values in this style +- some providers (for example Oracle setups) do not behave the same way +- forcing return-value extraction on such providers can cause runtime errors or invalid result handling + +If procedures fail on non-SQL Server providers, first disable `SupportStoredProceduresResult` and rely on explicit `out_` parameters for status/result codes. + ## Invocation Basics ```csharp @@ -232,7 +252,11 @@ var code = (int)res.code; - Mapping to class fails silently (dynamic fallback): - ensure output model is mappable and keys match columns/properties - Return value not appearing: - - ensure `ret_` parameter is supplied, or enable provider option `SupportStoredProceduresResult` + - ensure `ret_` parameter is supplied + - ensure provider option `SupportStoredProceduresResult` matches your DB behavior +- Procedure call errors on non-SQL Server providers: + - set `SupportStoredProceduresResult = false` + - return status via explicit `out_` parameters instead of return-value semantics ## Notes