Merge branch 'docs/full-markdown-manual'

This commit is contained in:
root
2026-02-26 09:03:53 +01:00
11 changed files with 1468 additions and 0 deletions

24
README.md Normal file
View File

@@ -0,0 +1,24 @@
# 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
- ADO.NET extensions and cached reader
- .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

38
docs/README.md Normal file
View File

@@ -0,0 +1,38 @@
# 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)
- [ADO.NET Extensions](ado-net-extensions.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<T>()`
- `db.Select<T>()`
- `db.Insert<T>() / db.Update<T>() / db.Delete<T>()`
The dynamic API is concise and fast to use. The fluent builder API gives stronger composition and explicit SQL control.

329
docs/ado-net-extensions.md Normal file
View File

@@ -0,0 +1,329 @@
# 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`.
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
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<T>()`
- `ExecuteScalarAs<T>(defaultValue)`
- `ExecuteScalarAs<T>(TryParseHandler<T>)`
- `ExecuteScalarAs<T>(defaultValue, TryParseHandler<T>)`
- `ExecuteEnumeratorOf<T>(defaultValue, TryParseHandler<T>)`
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<DynamicCachedReader, int, bool> 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<T>(...)`
- `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.
## 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<string, GenericMessage>();
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<Address>(source),
Destination = string.IsNullOrEmpty(destination) ? null : Deserialize<Address>(destination),
Priority = r.GetInt32IfNotNull("Priority") ?? 0,
};
}
```
Extension methods used:
- `SetCommand(...)`
- `AddParameter(...)`
- `GetStringIfNotNull`
- `GetDateTimeIfNotNull`
- `GetInt32IfNotNull`

118
docs/dynamic-table-api.md Normal file
View File

@@ -0,0 +1,118 @@
# Dynamic Table API
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`:
```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");
```
## Filtering with Named Arguments
```csharp
users.Count(first: "Ori");
users.Single(code: "101");
users.Query(columns: "id,first", id: 19);
```
## 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));
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", email: "juri.gagarin@megacorp.com");
users.Insert(values: new
{
code = "202",
first = "Juri",
last = "Gagarin",
email = "juri.gagarin@megacorp.com"
});
```
## 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));
var list = (users.Query(type: typeof(User)) as IEnumerable<object>)
.Cast<User>()
.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.
- Unknown dynamic operations throw `InvalidOperationException`.
- Explicit schema behavior depends on `DynamicDatabaseOptions.SupportSchema`.

263
docs/fluent-builder-api.md Normal file
View File

@@ -0,0 +1,263 @@
# Fluent Builder API
The fluent API is built around:
- `IDynamicSelectQueryBuilder`
- `IDynamicInsertQueryBuilder`
- `IDynamicUpdateQueryBuilder`
- `IDynamicDeleteQueryBuilder`
This page documents call semantics and parser nuances from:
- `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 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<User>("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
Subqueries can be embedded in select/where/join contexts.
```csharp
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));
```
Supported tested placements:
- `SELECT (subquery) AS alias`
- `WHERE column = (subquery)`
- `WHERE column IN (subquery)`
- `JOIN (subquery) AS alias ON ...`
## Typed Execution and Scalar
```csharp
var list = db.From<User>("u").Where(x => x.u.Active == true).Execute<User>().ToList();
var count = db.From<User>("u").Select(x => x.Count()).ScalarAs<int>();
```
## Practical Patterns
- 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`).

View File

@@ -0,0 +1,190 @@
# Mapping and Entities
DynamORM mapping is a core feature, not just a convenience layer.
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`
```csharp
[Table(Name = "users", Owner = "dbo", Override = true)]
public class User
{
// ...
}
```
Fields:
- `Name`: table name override.
- `Owner`: schema/owner segment.
- `Override`: prefer attribute schema over database schema (important when provider schema is limited).
## `ColumnAttribute`
```csharp
public class User
{
[Column("id", isKey: true)]
public int Id { get; set; }
[Column("email")]
public string Email { get; set; }
[Column("created_at", false, DbType.DateTime)]
public DateTime CreatedAt { get; set; }
}
```
Important flags:
- `IsKey`
- `AllowNull`
- `IsNoInsert`
- `IsNoUpdate`
- `Type`, `Size`, `Precision`, `Scale`
These influence generated parameters and update/insert behavior.
## Advanced Mapping Example
```csharp
[Table(Name = "users", Override = true)]
public class UserEntity : DynamicEntityBase
{
[Column("id", true, DbType.Int32)]
public int Id { get; set; }
[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
{
[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<Profile>().ValidateObject(profile);
```
Validation scenarios are verified in `DynamORM.Tests/Helpers/Validation/ObjectValidationTest.cs`.
## `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 => _first;
set
{
OnPropertyChanging(nameof(First), _first, value);
_first = value;
}
}
}
```
Typical flow:
```csharp
var user = db.From<UserEntity>()
.Where(x => x.id == 19)
.Execute<UserEntity>()
.First();
user.SetDynamicEntityState(DynamicEntityState.Existing);
user.First = "Yuri";
user.Save(db); // updates only changed fields when possible
```
## 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()`
- `GetByQuery(...)`
- `Insert(...)`
- `Update(...)`
- `Delete(...)`
- `Save(...)`
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

@@ -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.

65
docs/quick-start.md Normal file
View File

@@ -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<string>();
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`.

264
docs/stored-procedures.md Normal file
View File

@@ -0,0 +1,264 @@
# Stored Procedures
Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled.
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
var scalar = db.Procedures.sp_Exp_Scalar();
var scalarTyped = db.Procedures.sp_Exp_Scalar<int>();
```
Schema-qualified naming is built through dynamic chaining:
```csharp
var res = db.Procedures.dbo.reporting.MyProc();
```
Final command name is `dbo.reporting.MyProc`.
## Parameter Direction Prefixes
Because dynamic `out/ref` is limited, parameter direction is encoded by argument name prefix:
- `out_` => `ParameterDirection.Output`
- `ret_` => `ParameterDirection.ReturnValue`
- `both_` => `ParameterDirection.InputOutput`
Example:
```csharp
dynamic res = db.Procedures.sp_Message_SetState(
id: "abc-001",
both_state: "pending",
out_message: "",
ret_code: 0);
```
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
dynamic res = db.Procedures.sp_Message_SetState(
id: "abc-001",
out_message: new DynamicSchemaColumn
{
Name = "message",
Type = DbType.String,
Size = 1024,
},
ret_code: 0);
```
## Option B: `DynamicColumn` (recommended for input/output with value)
Use this when you need direction + value + schema in one object.
```csharp
dynamic res = db.Procedures.sp_Message_SetState(
both_state: new DynamicColumn
{
ColumnName = "state",
ParameterDirection = ParameterDirection.InputOutput,
Value = "pending",
Schema = new DynamicSchemaColumn
{
Name = "state",
Type = DbType.String,
Size = 32,
}
});
```
## Option C: Plain value + name prefix
Quickest form, but type/size inference is automatic.
```csharp
dynamic res = db.Procedures.sp_Message_SetState(
id: "abc-001",
out_message: "",
ret_code: 0);
```
Use Option A/B whenever output size/type must be controlled.
## Return Shape Matrix (What You Actually Get)
`DynamicProcedureInvoker` chooses result shape from generic type arguments.
## 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<TMain>(...)`)
Main result type resolution:
- `TMain == IDataReader` => returns cached reader (`DynamicCachedReader`)
- `TMain == DataTable` => returns `DataTable`
- `TMain == List<object>` or `IEnumerable<object>` => list of dynamic rows
- `TMain == List<primitive/string>` => converted first-column list
- `TMain == List<complex>` => 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<TMain, TOutModel>(...)`)
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<int, MessageSetStateResult>(
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<int>();
```
## Pattern 2: Output params + mapped output model
```csharp
var res = db.Procedures.sp_Message_SetState<int, MessageSetStateResult>(
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<DataTable>(batchId: 10);
```
## Pattern 4: Procedure returning mapped collection
```csharp
List<MessageRow> rows = db.Procedures.sp_Message_List<List<MessageRow>>(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
- 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
- Behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs`.
- XML examples also appear in `DynamORM/DynamicDatabase.cs`.

View File

@@ -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<T>()`
- projection with `MapEnumerable<T>()`
- 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.

View File

@@ -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.