diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc50393 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# DynamORM + +Dynamic ORM and SQL builder for .NET, with dynamic and typed APIs. + +## Documentation + +Full documentation is available in [`docs/README.md`](docs/README.md): + +- Quick start +- Dynamic table API +- Fluent builder API +- Mapping and entity lifecycle +- Transaction/disposal semantics +- Stored procedures +- .NET 4.0 amalgamation workflow +- Test-driven examples + +## Repository Layout + +- `DynamORM/`: core library +- `DynamORM.Tests/`: test suite +- `AmalgamationTool/`: amalgamation generator and generated single-file output +- `DynamORM.Net40.csproj`: net40 build for amalgamated source compatibility diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ce2fc63 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,37 @@ +# DynamORM Documentation + +This documentation is based on: +- XML comments in the library source (`DynamORM/*.cs`, `DynamORM/*/*.cs`). +- Executable usage patterns from the test suite (`DynamORM.Tests`). + +## Contents + +- [Quick Start](quick-start.md) +- [Dynamic Table API](dynamic-table-api.md) +- [Fluent Builder API](fluent-builder-api.md) +- [Mapping and Entities](mapping-and-entities.md) +- [Transactions and Disposal](transactions-and-disposal.md) +- [Stored Procedures](stored-procedures.md) +- [.NET 4.0 Amalgamation](net40-amalgamation.md) +- [Test-Driven Examples](test-driven-examples.md) + +## Supported Targets + +Main library targets are defined in `DynamORM/DynamORM.csproj`. + +For legacy .NET 4.0 consumers, use the amalgamated source workflow documented in [.NET 4.0 Amalgamation](net40-amalgamation.md). + +## Design Overview + +DynamORM provides two major usage styles: + +- Dynamic table access: + - `db.Table("users").Count(...)` + - `db.Table("users").Insert(...)` + - `db.Table("users").Query(...)` +- Fluent SQL builder access: + - `db.From()` + - `db.Select()` + - `db.Insert() / db.Update() / db.Delete()` + +The dynamic API is concise and fast to use. The fluent builder API gives stronger composition and explicit SQL control. diff --git a/docs/dynamic-table-api.md b/docs/dynamic-table-api.md new file mode 100644 index 0000000..61ec861 --- /dev/null +++ b/docs/dynamic-table-api.md @@ -0,0 +1,80 @@ +# Dynamic Table API + +The dynamic table API centers on `DynamicTable` and allows expressive calls like: + +```csharp +dynamic users = db.Table("users"); +``` + +## Read Operations + +Examples backed by `DynamORM.Tests/Select/DynamicAccessTests.cs`: + +```csharp +users.Count(columns: "id"); +users.First(columns: "id"); +users.Last(columns: "id"); +users.Min(columns: "id"); +users.Max(columns: "id"); +users.Avg(columns: "id"); +users.Sum(columns: "id"); +users.Scalar(columns: "first", id: 19); +users.Single(id: 19); +users.Query(columns: "first,last", order: "id:desc"); +``` + +## Conditions with `DynamicColumn` + +```csharp +users.Count(where: new DynamicColumn("id").Greater(100)); +users.Count(where: new DynamicColumn("login").Like("Hoyt.%")); +users.Count(where: new DynamicColumn("id").Between(75, 100)); +users.Count(where: new DynamicColumn("id").In(75, 99, 100)); +``` + +## Insert + +```csharp +users.Insert(code: "201", first: "Juri", last: "Gagarin"); + +users.Insert(values: new +{ + code = "202", + first = "Juri", + last = "Gagarin" +}); +``` + +## Update + +```csharp +users.Update(id: 1, first: "Yuri", last: "Gagarin"); + +users.Update( + values: new { first = "Yuri" }, + where: new { id = 1 }); +``` + +## Delete + +```csharp +users.Delete(code: "201"); +users.Delete(where: new { id = 14, code = 14 }); +``` + +## Typed Dynamic Table Calls + +`DynamicTable` methods also accept `type: typeof(T)` for mapped class scenarios: + +```csharp +users.Count(type: typeof(User), columns: "id"); +users.Query(type: typeof(User)); +``` + +These usage patterns are covered in `DynamORM.Tests/Select/TypedAccessTests.cs`. + +## Notes + +- Dynamic member names map to table/column names and builder conventions. +- Unknown dynamic operations throw `InvalidOperationException`. +- Explicit schema behavior depends on `DynamicDatabaseOptions.SupportSchema`. diff --git a/docs/fluent-builder-api.md b/docs/fluent-builder-api.md new file mode 100644 index 0000000..57985d7 --- /dev/null +++ b/docs/fluent-builder-api.md @@ -0,0 +1,84 @@ +# Fluent Builder API + +The fluent API is built around interfaces like `IDynamicSelectQueryBuilder`, `IDynamicInsertQueryBuilder`, `IDynamicUpdateQueryBuilder`, and `IDynamicDeleteQueryBuilder`. + +## Core Select Flow + +```csharp +using (var query = db.From("users", "u") + .Where("u.id", 19) + .SelectColumn("u.first", "u.last")) +{ + var result = query.Execute().ToList(); +} +``` + +## Typed Select + +```csharp +using (var query = db.From("u") + .Where(x => x.u.id == 19) + .Select(x => x.u.All())) +{ + var users = query.Execute().ToList(); +} +``` + +## Parser Lambda Patterns + +The parser supports patterns tested in `DynamORM.Tests/Select/ParserTests.cs`: + +- `From(x => x.dbo.Users)` +- `From(x => x.dbo.Users.As(x.u))` +- `Join(x => x.Left().Accounts.As(x.a).On(x.a.userId == x.u.id))` +- `Where(x => x.Or(x.u.id > 100))` +- `Select(x => x.u.first.As(x.firstName))` +- `GroupBy(x => x.u.last)` +- `Having(x => x.Count() > 1)` +- `OrderBy(x => x.u.id.Desc())` + +## Subqueries + +```csharp +var sub = new DynamicSelectQueryBuilder(db).From(x => x.dbo.Users); + +using (var query = new DynamicSelectQueryBuilder(db) + .From(x => x(sub).As("u")) + .Select(x => x.u.All())) +{ + var rows = query.Execute().ToList(); +} +``` + +## Scalar Helpers + +```csharp +var count = db.From() + .Select(x => x.Count()) + .ScalarAs(); +``` + +## Modify Builders + +```csharp +db.Insert("users").Values("code", "301").Values("first", "Ada").Execute(); + +db.Update("users") + .Values("first", "Alicia") + .Where("id", 301) + .Execute(); + +db.Delete("users") + .Where("id", 301) + .Execute(); +``` + +## SQL Inspection + +You can inspect generated SQL from builder objects: + +```csharp +var sql = query.CommandText(); +``` + +Command text assertions are heavily used in parser tests. diff --git a/docs/mapping-and-entities.md b/docs/mapping-and-entities.md new file mode 100644 index 0000000..319617e --- /dev/null +++ b/docs/mapping-and-entities.md @@ -0,0 +1,101 @@ +# Mapping and Entities + +DynamORM supports attribute-driven mapping and entity lifecycle helpers. + +## Mapping Attributes + +## `TableAttribute` + +```csharp +[Table(Name = "users", Owner = "dbo", Override = true)] +public class User +{ + // ... +} +``` + +- `Name`: table name override. +- `Owner`: schema/owner segment. +- `Override`: prefer attribute schema info over provider schema. + +## `ColumnAttribute` + +```csharp +public class User +{ + [Column("id", isKey: true)] + public int Id { get; set; } + + [Column("email")] + public string Email { get; set; } +} +``` + +Important flags: + +- `IsKey` +- `AllowNull` +- `IsNoInsert` +- `IsNoUpdate` +- `Type`, `Size`, `Precision`, `Scale` + +## Ignore Fields + +Use `IgnoreAttribute` to skip properties in mapper-driven workflows. + +## Validation + +`RequiredAttribute` can define value rules. + +```csharp +public class Profile +{ + [Required(1f, 10f)] + public int Rank { get; set; } + + [Required(2, true)] + [Required(7, 18, ElementRequirement = true)] + public decimal[] Scores { get; set; } +} + +var issues = DynamicMapperCache.GetMapper().ValidateObject(profile); +``` + +Validation scenarios are verified in `DynamORM.Tests/Helpers/Validation/ObjectValidationTest.cs`. + +## `DynamicEntityBase` + +`DynamicEntityBase` tracks state and changed fields. + +```csharp +public class UserEntity : DynamicEntityBase +{ + [Column("id", true)] + public int Id { get; set; } + + [Column("first")] + public string First { get; set; } +} +``` + +Available behaviors: + +- `Validate()` +- `Insert(db)` +- `Update(db)` +- `Delete(db)` +- `Refresh(db)` +- `Save(db)` driven by `DynamicEntityState` + +## Repository Base + +`DynamicRepositoryBase` provides common operations: + +- `GetAll()` +- `GetByQuery(...)` +- `Insert(...)` +- `Update(...)` +- `Delete(...)` +- `Save(...)` + +It ensures query/table compatibility for `T` unless explicitly bypassed. diff --git a/docs/net40-amalgamation.md b/docs/net40-amalgamation.md new file mode 100644 index 0000000..f050cdc --- /dev/null +++ b/docs/net40-amalgamation.md @@ -0,0 +1,42 @@ +# .NET 4.0 Amalgamation + +Legacy .NET 4.0 consumers should use the amalgamated source model. + +## Files + +- `AmalgamationTool/DynamORM.Amalgamation.cs`: merged source file. +- `DynamORM.Net40.csproj`: net40 build project that compiles the amalgamation output. + +## Regenerate Amalgamation + +The amalgamation file must be regenerated after library source changes. + +Current workflow in this repository: + +1. Build or run `AmalgamationTool` (or use an equivalent merge script). +2. Merge all files from `DynamORM/` into `AmalgamationTool/DynamORM.Amalgamation.cs`. +3. Build `DynamORM.Net40.csproj`. + +## Build in Mono Container + +```bash +docker run --rm -v "$PWD":/src -w /src mono:latest \ + bash -lc "msbuild /t:Build /p:Configuration=Release DynamORM.Net40.csproj" +``` + +Expected output artifact: + +- `bin/Release/net40/DynamORM.Net40.dll` + +## Build with .NET SDK Container + +```bash +docker run --rm -v "$PWD":/src -w /src mcr.microsoft.com/dotnet/sdk:10.0-preview \ + dotnet build DynamORM.Net40.csproj -c Release +``` + +## Important Constraint + +Do not force net40 into the main multi-target `DynamORM/DynamORM.csproj` for modern workflows. + +Keep net40 compatibility validated through the dedicated amalgamation project so legacy consumers can embed the single-file source without destabilizing primary targets. diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..4a24707 --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,65 @@ +# Quick Start + +## Install and Reference + +Reference the `DynamORM` project or package from your application. + +## Basic Setup + +```csharp +using System.Data.SQLite; +using DynamORM; + +var options = + DynamicDatabaseOptions.SingleConnection | + DynamicDatabaseOptions.SingleTransaction | + DynamicDatabaseOptions.SupportLimitOffset | + DynamicDatabaseOptions.SupportSchema; + +using (var db = new DynamicDatabase( + SQLiteFactory.Instance, + "Data Source=app.db;", + options)) +{ + db.DumpCommands = true; + + var users = db.Table("users"); + var total = users.Count(columns: "id"); + var first = users.First(columns: "id,first,last"); +} +``` + +This setup mirrors `DynamORM.Tests/TestsBase.cs`. + +## First Query (Dynamic API) + +```csharp +using (var db = new DynamicDatabase(SQLiteFactory.Instance, "Data Source=app.db;", options)) +{ + var row = db.Table("users").Single(id: 19); + Console.WriteLine(row.first); +} +``` + +## First Query (Fluent Builder API) + +```csharp +using (var db = new DynamicDatabase(SQLiteFactory.Instance, "Data Source=app.db;", options)) +using (var query = db.From("users").Where("id", 19).SelectColumn("first")) +{ + var first = query.ScalarAs(); + Console.WriteLine(first); +} +``` + +## Insert, Update, Delete + +```csharp +var table = db.Table("users"); + +table.Insert(code: "201", first: "Juri", last: "Gagarin"); +table.Update(values: new { first = "Yuri" }, where: new { code = "201" }); +table.Delete(code: "201"); +``` + +These forms are validated in `DynamORM.Tests/Modify/DynamicModificationTests.cs`. diff --git a/docs/stored-procedures.md b/docs/stored-procedures.md new file mode 100644 index 0000000..0a5e09e --- /dev/null +++ b/docs/stored-procedures.md @@ -0,0 +1,51 @@ +# Stored Procedures + +Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled. + +## Basic Invocation + +```csharp +var scalar = db.Procedures.sp_Exp_Scalar(); +var scalarTyped = db.Procedures.sp_Exp_Scalar(); +``` + +## Input, Output, Return Parameters + +Prefixes in argument names control parameter direction: + +- `out_` for output +- `ret_` for return value +- `both_` for input/output + +Example pattern: + +```csharp +var res = db.Procedures.sp_Exp_SomeInputAndOutput< + string, + MyProcResult>( + Name: "G4g4r1n", + out_Result: new DynamicSchemaColumn { Size = 256 }, + ret_Return: 0); +``` + +## Nested Procedure Names + +Dynamic member chaining builds qualified names: + +```csharp +var res = db.Procedures.dbo.reporting.MyProc(); +``` + +This resolves to `dbo.reporting.MyProc`. + +## Result Mapping + +If generic return types are provided, DynamORM attempts mapper-based projection into the target type. + +If output parameters are present, result payload is assembled from: + +- main scalar/resultset-derived value +- output values +- optional return value + +The behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs` and documented in XML examples in `DynamORM/DynamicDatabase.cs`. diff --git a/docs/test-driven-examples.md b/docs/test-driven-examples.md new file mode 100644 index 0000000..27daf66 --- /dev/null +++ b/docs/test-driven-examples.md @@ -0,0 +1,70 @@ +# Test-Driven Examples + +This page maps concrete examples to test files so behavior can be verified quickly. + +## Dynamic Select and Aggregates + +Source: `DynamORM.Tests/Select/DynamicAccessTests.cs` + +- `Count/Min/Max/Avg/Sum/Scalar` +- `First/Last/Single` +- `IN`, `LIKE`, `BETWEEN`, comparison operators +- ad-hoc column expressions and aliases + +## Typed Access and Mapping + +Source: `DynamORM.Tests/Select/TypedAccessTests.cs` + +- `Query(type: typeof(T))` +- `Execute()` +- projection with `MapEnumerable()` +- table mapping and strongly typed expressions + +## SQL Parser and CommandText Expectations + +Source: `DynamORM.Tests/Select/ParserTests.cs` + +- `From` variants (`schema.table`, aliasing, string forms) +- join expressions and join kind helpers +- subquery embedding +- `NoLock()` translation +- deterministic SQL text generation checks + +## Data Modification + +Source: `DynamORM.Tests/Modify/DynamicModificationTests.cs` + +- insert via named args, anonymous object, mapped class, plain class +- update via key/object/where +- delete via key/object/where + +## Schema Variants + +Sources: + +- `DynamORM.Tests/Modify/DynamicTypeSchemaModificationTests.cs` +- `DynamORM.Tests/Modify/DynamicNoSchemaModificationTests.cs` +- `DynamORM.Tests/Select/DynamicTypeSchemaAccessTests.cs` +- `DynamORM.Tests/Select/DynamicNoSchemaAccessTests.cs` + +Use these to validate behavior with and without schema support. + +## Resource and Pooling Semantics + +Source: `DynamORM.Tests/Helpers/PoolingTests.cs` + +- command invalidation after database disposal +- transaction rollback behavior during disposal + +## Validation + +Source: `DynamORM.Tests/Helpers/Validation/ObjectValidationTest.cs` + +- rule-based object validation with `RequiredAttribute` +- array element-level validation + +## Dynamic Parser (Helper Layer) + +Source: `DynamORM.Tests/Helpers/Dynamic/DynamicParserTests.cs` + +Covers lower-level parser behavior used by fluent lambda parsing. diff --git a/docs/transactions-and-disposal.md b/docs/transactions-and-disposal.md new file mode 100644 index 0000000..694b835 --- /dev/null +++ b/docs/transactions-and-disposal.md @@ -0,0 +1,65 @@ +# Transactions and Disposal + +DynamORM manages connections, command pools, and transaction stacks internally. + +## Connection and Transaction Options + +`DynamicDatabaseOptions` controls behavior: + +- `SingleConnection` +- `SingleTransaction` +- `SupportSchema` +- `SupportStoredProcedures` +- `SupportNoLock` +- `SupportTop` / `SupportLimitOffset` / `SupportFirstSkip` + +Typical setup: + +```csharp +var options = + DynamicDatabaseOptions.SingleConnection | + DynamicDatabaseOptions.SingleTransaction | + DynamicDatabaseOptions.SupportLimitOffset | + DynamicDatabaseOptions.SupportSchema; +``` + +## Transaction Usage + +```csharp +using (var db = new DynamicDatabase(factory, connectionString, options)) +using (var conn = db.Open()) +using (var tx = conn.BeginTransaction()) +using (var cmd = conn.CreateCommand()) +{ + cmd.SetCommand("UPDATE users SET first = 'Ada' WHERE id = 1").ExecuteNonQuery(); + tx.Commit(); +} +``` + +Global transaction mode is also available via `db.BeginTransaction()`. + +## Disposal Guarantees + +Current disposal behavior includes idempotent guards on key objects: + +- `DynamicDatabase` +- `DynamicConnection` +- `DynamicCommand` +- `DynamicTransaction` +- `DynamicTable` +- builder and parser helpers + +This prevents repeated cleanup from throwing or re-disposing lower-level resources. + +## Pooling and Rollback Behavior + +Behavior validated by `DynamORM.Tests/Helpers/PoolingTests.cs`: + +- Disposing the database invalidates active commands. +- Open transactions are rolled back during disposal when not committed. + +## Practices + +- Prefer `using` blocks for `DynamicDatabase`, connections, commands, transactions, and builders. +- Do not manually re-dispose the same object from multiple ownership paths unless `IsDisposed` is checked. +- Keep transaction scope short and explicit.