Integrate deep-dive content into API docs and expand mapping

This commit is contained in:
root
2026-02-26 08:06:56 +01:00
parent 2c25e78bef
commit b628348af8
7 changed files with 267 additions and 230 deletions

View File

@@ -9,7 +9,6 @@ Full documentation is available in [`docs/README.md`](docs/README.md):
- Quick start - Quick start
- Dynamic table API - Dynamic table API
- Fluent builder API - Fluent builder API
- Syntax and procedure deep dive
- Mapping and entity lifecycle - Mapping and entity lifecycle
- Transaction/disposal semantics - Transaction/disposal semantics
- Stored procedures - Stored procedures

View File

@@ -9,7 +9,6 @@ This documentation is based on:
- [Quick Start](quick-start.md) - [Quick Start](quick-start.md)
- [Dynamic Table API](dynamic-table-api.md) - [Dynamic Table API](dynamic-table-api.md)
- [Fluent Builder API](fluent-builder-api.md) - [Fluent Builder API](fluent-builder-api.md)
- [Syntax and Procedure Deep Dive](syntax-and-procedures-deep-dive.md)
- [Mapping and Entities](mapping-and-entities.md) - [Mapping and Entities](mapping-and-entities.md)
- [Transactions and Disposal](transactions-and-disposal.md) - [Transactions and Disposal](transactions-and-disposal.md)
- [Stored Procedures](stored-procedures.md) - [Stored Procedures](stored-procedures.md)

View File

@@ -1,11 +1,13 @@
# Dynamic Table API # 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 ```csharp
dynamic users = db.Table("users"); dynamic users = db.Table("users");
``` ```
This API is best when table/column selection is dynamic or you want very short CRUD code.
## Read Operations ## Read Operations
Examples backed by `DynamORM.Tests/Select/DynamicAccessTests.cs`: Examples backed by `DynamORM.Tests/Select/DynamicAccessTests.cs`:
@@ -23,6 +25,14 @@ users.Single(id: 19);
users.Query(columns: "first,last", order: "id:desc"); 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` ## Conditions with `DynamicColumn`
```csharp ```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("login").Like("Hoyt.%"));
users.Count(where: new DynamicColumn("id").Between(75, 100)); 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(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 ## Insert
```csharp ```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 users.Insert(values: new
{ {
code = "202", code = "202",
first = "Juri", first = "Juri",
last = "Gagarin" last = "Gagarin",
email = "juri.gagarin@megacorp.com"
}); });
``` ```
@@ -69,10 +93,24 @@ users.Delete(where: new { id = 14, code = 14 });
```csharp ```csharp
users.Count(type: typeof(User), columns: "id"); users.Count(type: typeof(User), columns: "id");
users.Query(type: typeof(User)); users.Query(type: typeof(User));
var list = (users.Query(type: typeof(User)) as IEnumerable<object>)
.Cast<User>()
.ToList();
``` ```
These usage patterns are covered in `DynamORM.Tests/Select/TypedAccessTests.cs`. 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 ## Notes
- Dynamic member names map to table/column names and builder conventions. - Dynamic member names map to table/column names and builder conventions.

View File

@@ -1,6 +1,13 @@
# Fluent Builder API # 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 ## Core Select Flow
@@ -26,10 +33,11 @@ using (var query = db.From<User>("u")
## Parser Lambda Patterns ## 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)`
- `From(x => x.dbo.Users.As(x.u))` - `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))` - `Join(x => x.Left().Accounts.As(x.a).On(x.a.userId == x.u.id))`
- `Where(x => x.Or(x.u.id > 100))` - `Where(x => x.Or(x.u.id > 100))`
- `Select(x => x.u.first.As(x.firstName))` - `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)` - `Having(x => x.Count() > 1)`
- `OrderBy(x => x.u.id.Desc())` - `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 ## Subqueries
```csharp ```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) using (var query = new DynamicSelectQueryBuilder(db)
.From(x => x(sub).As("u")) .From(x => x(sub).As("u"))
@@ -61,7 +87,10 @@ var count = db.From<User>()
## Modify Builders ## Modify Builders
```csharp ```csharp
db.Insert("users").Values("code", "301").Values("first", "Ada").Execute(); db.Insert("users")
.Values("code", "301")
.Values("first", "Ada")
.Execute();
db.Update("users") db.Update("users")
.Values("first", "Alicia") .Values("first", "Alicia")
@@ -73,6 +102,15 @@ db.Delete("users")
.Execute(); .Execute();
``` ```
Typed variant:
```csharp
db.Insert<User>()
.Values(x => x.code, "302")
.Values(x => x.first, "Grace")
.Execute();
```
## SQL Inspection ## SQL Inspection
You can inspect generated SQL from builder objects: You can inspect generated SQL from builder objects:

View File

@@ -1,8 +1,26 @@
# Mapping and Entities # 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<T>()`)
- dynamic projection to classes
- entity lifecycle operations in `DynamicEntityBase`
- procedure result mapping
- validation (`ValidateObject`)
## `TableAttribute` ## `TableAttribute`
@@ -14,9 +32,11 @@ public class User
} }
``` ```
Fields:
- `Name`: table name override. - `Name`: table name override.
- `Owner`: schema/owner segment. - `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` ## `ColumnAttribute`
@@ -28,6 +48,9 @@ public class User
[Column("email")] [Column("email")]
public string Email { get; set; } public string Email { get; set; }
[Column("created_at", false, DbType.DateTime)]
public DateTime CreatedAt { get; set; }
} }
``` ```
@@ -39,13 +62,40 @@ Important flags:
- `IsNoUpdate` - `IsNoUpdate`
- `Type`, `Size`, `Precision`, `Scale` - `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 ```csharp
public class Profile public class Profile
@@ -63,33 +113,65 @@ var issues = DynamicMapperCache.GetMapper<Profile>().ValidateObject(profile);
Validation scenarios are verified in `DynamORM.Tests/Helpers/Validation/ObjectValidationTest.cs`. Validation scenarios are verified in `DynamORM.Tests/Helpers/Validation/ObjectValidationTest.cs`.
## `DynamicEntityBase` ## `DynamicEntityBase` Lifecycle
`DynamicEntityBase` tracks state and changed fields. `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 ```csharp
public class UserEntity : DynamicEntityBase public class UserEntity : DynamicEntityBase
{ {
private string _first;
[Column("id", true)] [Column("id", true)]
public int Id { get; set; } public int Id { get; set; }
[Column("first")] [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()` ```csharp
- `Insert(db)` var user = db.From<UserEntity>()
- `Update(db)` .Where(x => x.id == 19)
- `Delete(db)` .Execute<UserEntity>()
- `Refresh(db)` .First();
- `Save(db)` driven by `DynamicEntityState`
## Repository Base user.SetDynamicEntityState(DynamicEntityState.Existing);
user.First = "Yuri";
user.Save(db); // updates only changed fields when possible
```
`DynamicRepositoryBase<T>` 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<T>`)
`DynamicRepositoryBase<T>` provides:
- `GetAll()` - `GetAll()`
- `GetByQuery(...)` - `GetByQuery(...)`
@@ -98,4 +180,11 @@ Available behaviors:
- `Delete(...)` - `Delete(...)`
- `Save(...)` - `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.

View File

@@ -2,8 +2,6 @@
Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled. 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 ## Basic Invocation
```csharp ```csharp
@@ -11,26 +9,7 @@ var scalar = db.Procedures.sp_Exp_Scalar();
var scalarTyped = db.Procedures.sp_Exp_Scalar<int>(); var scalarTyped = db.Procedures.sp_Exp_Scalar<int>();
``` ```
## Input, Output, Return Parameters ## Schema-Qualified Invocation
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: Dynamic member chaining builds qualified names:
@@ -40,14 +19,82 @@ var res = db.Procedures.dbo.reporting.MyProc();
This resolves to `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 Example:
- output values
- optional return value
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<string, ProcResult>(
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<object>`: dynamic row enumeration.
- `T == IEnumerable<primitive>`: converts first column of each row.
- `T == IEnumerable<class>`: maps rows via mapper cache.
- `T == class`: maps structured result to a class.
Examples:
```csharp
IDataReader rdr = db.Procedures.MyProc<IDataReader>();
DataTable table = db.Procedures.MyProc<DataTable>();
List<int> ids = db.Procedures.MyProc<List<int>>();
List<User> users = db.Procedures.MyProc<List<User>>();
User user = db.Procedures.MyProc<User>();
```
## 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`.

View File

@@ -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<dynamic>).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<Users>("u");
```
Composable select:
```csharp
using (var query = db.From<Users>("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<T>()`, `ScalarAs<T>()`).
## 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<int>();
```
## 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<string, MyProcResult>(
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<object>`: dynamic row enumeration.
- `T == IEnumerable<primitive>`: scalar conversion per row.
- `T == IEnumerable<class>`: 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).