Merge branch 'docs/full-markdown-manual'
This commit is contained in:
24
README.md
Normal file
24
README.md
Normal 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
38
docs/README.md
Normal 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
329
docs/ado-net-extensions.md
Normal 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
118
docs/dynamic-table-api.md
Normal 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
263
docs/fluent-builder-api.md
Normal 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`).
|
||||
190
docs/mapping-and-entities.md
Normal file
190
docs/mapping-and-entities.md
Normal 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.
|
||||
42
docs/net40-amalgamation.md
Normal file
42
docs/net40-amalgamation.md
Normal 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
65
docs/quick-start.md
Normal 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
264
docs/stored-procedures.md
Normal 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`.
|
||||
70
docs/test-driven-examples.md
Normal file
70
docs/test-driven-examples.md
Normal 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.
|
||||
65
docs/transactions-and-disposal.md
Normal file
65
docs/transactions-and-disposal.md
Normal 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.
|
||||
Reference in New Issue
Block a user