From e95bbd0f67067a76720d0fad19d2b755595edb39 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 08:30:43 +0100 Subject: [PATCH] Expand stored procedure docs with result-shape and output parameter guidance --- docs/stored-procedures.md | 242 ++++++++++++++++++++++++++++++-------- 1 file changed, 191 insertions(+), 51 deletions(-) diff --git a/docs/stored-procedures.md b/docs/stored-procedures.md index 4ddeabd..4b6820d 100644 --- a/docs/stored-procedures.md +++ b/docs/stored-procedures.md @@ -2,99 +2,239 @@ Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled. -## Basic Invocation +This page documents actual runtime behavior from `DynamicProcedureInvoker`. + +## Invocation Basics ```csharp var scalar = db.Procedures.sp_Exp_Scalar(); var scalarTyped = db.Procedures.sp_Exp_Scalar(); ``` -## Schema-Qualified Invocation - -Dynamic member chaining builds qualified names: +Schema-qualified naming is built through dynamic chaining: ```csharp var res = db.Procedures.dbo.reporting.MyProc(); ``` -This resolves to `dbo.reporting.MyProc`. +Final command name is `dbo.reporting.MyProc`. -## Input, Output, Return, InputOutput Parameters +## Parameter Direction Prefixes -Prefixes in argument names control parameter direction: +Because dynamic `out/ref` is limited, parameter direction is encoded by argument name prefix: -- `out_` for output -- `ret_` for return value -- `both_` for input/output +- `out_` => `ParameterDirection.Output` +- `ret_` => `ParameterDirection.ReturnValue` +- `both_` => `ParameterDirection.InputOutput` Example: ```csharp -dynamic res = db.Procedures.sp_Test_Scalar_In_Out( - inp: Guid.NewGuid(), - out_outp: Guid.Empty, - ret_Return: 0); +dynamic res = db.Procedures.sp_Message_SetState( + id: "abc-001", + both_state: "pending", + out_message: "", + ret_code: 0); ``` -## Using `DynamicSchemaColumn` for Explicit Output Shape +Prefix is removed from the exposed output key (`out_message` -> `message`). + +## How to Specify Type/Length for Out Parameters + +This is the most common pain point. You have 2 primary options. + +## Option A: `DynamicSchemaColumn` (recommended for output-only) + +Use this when you need explicit type, length, precision, or scale. ```csharp -var res = db.Procedures.sp_Exp_SomeInputAndOutput( - Name: "G4g4r1n", - out_Result: new DynamicSchemaColumn +dynamic res = db.Procedures.sp_Message_SetState( + id: "abc-001", + out_message: new DynamicSchemaColumn { - Name = "Result", - Size = 256 + Name = "message", + Type = DbType.String, + Size = 1024, }, - ret_Return: 0); + ret_code: 0); ``` -## Using `DynamicColumn` for Direction + Value + Schema +## Option B: `DynamicColumn` (recommended for input/output with value) + +Use this when you need direction + value + schema in one object. ```csharp -var res = db.Procedures.sp_WithInputOutput( - both_State: new DynamicColumn +dynamic res = db.Procedures.sp_Message_SetState( + both_state: new DynamicColumn { - ColumnName = "State", + ColumnName = "state", ParameterDirection = ParameterDirection.InputOutput, Value = "pending", - Schema = new DynamicSchemaColumn { Name = "State", Size = 32 } + Schema = new DynamicSchemaColumn + { + Name = "state", + Type = DbType.String, + Size = 32, + } }); ``` -## Result Shapes +## Option C: Plain value + name prefix -From `DynamicProcedureInvoker` behavior: - -- `T == IDataReader`: returns `CachedReader()` result. -- `T == DataTable`: materializes via `ToDataTable(...)`. -- `T == IEnumerable`: dynamic row enumeration. -- `T == IEnumerable`: converts first column of each row. -- `T == IEnumerable`: maps rows via mapper cache. -- `T == class`: maps structured result to a class. - -Examples: +Quickest form, but type/size inference is automatic. ```csharp -IDataReader rdr = db.Procedures.MyProc(); -DataTable table = db.Procedures.MyProc(); -List ids = db.Procedures.MyProc>(); -List users = db.Procedures.MyProc>(); -User user = db.Procedures.MyProc(); +dynamic res = db.Procedures.sp_Message_SetState( + id: "abc-001", + out_message: "", + ret_code: 0); ``` -## Output and Return Value Aggregation +Use Option A/B whenever output size/type must be controlled. -When output and/or return values are used, DynamORM aggregates: +## Return Shape Matrix (What You Actually Get) -- main result -- output parameters -- return value +`DynamicProcedureInvoker` chooses result shape from generic type arguments. -into a dynamic object or mapped class (if a target type is provided). +## No generic type arguments (`db.Procedures.MyProc(...)`) + +Main result path: + +- procedure executes via `ExecuteNonQuery()` +- if return-value support is enabled and return param is present, that value replaces affected-row count + +Final returned object: + +- if no out/ret params were requested: scalar main result (`int` or return value) +- if any out/ret/both params were requested: dynamic object with keys + +## One generic type argument (`MyProc(...)`) + +Main result type resolution: + +- `TMain == IDataReader` => returns cached reader (`DynamicCachedReader`) +- `TMain == DataTable` => returns `DataTable` +- `TMain == List` or `IEnumerable` => list of dynamic rows +- `TMain == List` => converted first-column list +- `TMain == List` => mapped list +- `TMain == primitive/string` => scalar converted value +- `TMain == complex class` => first row mapped to class (or `null`) + +Final returned object: + +- if no out/ret params were requested: `TMain` result directly +- if out/ret params exist: dynamic object containing main result + out params + +Important nuance: + +- when out params exist and only one generic type is provided, the result is a dynamic object, not bare `TMain`. + +## Two generic type arguments (`MyProc(...)`) + +This is the preferred pattern when out params are involved. + +- `TMain` is resolved as above +- out/main values are packed into a dictionary-like dynamic payload +- if mapper for `TOutModel` exists, payload is mapped to `TOutModel` +- otherwise fallback is dynamic object + +## Result Key Names in Out Payload + +When out payload is used: + +- main result is stored under procedure method name key (`binder.Name`) +- each out/both/ret param is stored under normalized parameter name (without prefix) + +Example call: + +```csharp +var res = db.Procedures.sp_Message_SetState( + id: "abc-001", + out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 }, + ret_code: 0); +``` + +Expected payload keys before mapping: + +- `sp_Message_SetState` (main result) +- `message` +- `code` + +## Preparing Result Classes Correctly + +Use `ColumnAttribute` to map returned payload keys. + +```csharp +public class MessageSetStateResult +{ + [Column("sp_Message_SetState")] + public int MainResult { get; set; } + + [Column("message")] + public string Message { get; set; } + + [Column("code")] + public int ReturnCode { get; set; } +} +``` + +If key names and property names already match, attributes are optional. + +## Common Patterns + +## Pattern 1: Main scalar only + +```csharp +int count = db.Procedures.sp_CountMessages(); +``` + +## Pattern 2: Output params + mapped output model + +```csharp +var res = db.Procedures.sp_Message_SetState( + id: "abc-001", + status: 2, + out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 }, + ret_code: 0); +``` + +## Pattern 3: Procedure returning dataset as table + +```csharp +DataTable dt = db.Procedures.sp_Message_GetBatch(batchId: 10); +``` + +## Pattern 4: Procedure returning mapped collection + +```csharp +List rows = db.Procedures.sp_Message_List>(status: 1); +``` + +## Pattern 5: Read output dynamically + +```csharp +dynamic res = db.Procedures.sp_Message_SetState( + id: "abc-001", + out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 }, + ret_code: 0); + +var message = (string)res.message; +var code = (int)res.code; +``` + +## Troubleshooting Checklist + +- Out value is truncated or null: + - define output schema explicitly with `DynamicSchemaColumn` (type + size) +- Unexpected return object shape: + - check whether any out/ret/both parameter was passed + - if yes, expect out payload object unless you used 2-generic mapping variant +- Mapping to class fails silently (dynamic fallback): + - ensure output model is mappable and keys match columns/properties +- Return value not appearing: + - ensure `ret_` parameter is supplied, or enable provider option `SupportStoredProceduresResult` ## Notes -- Enable `DynamicDatabaseOptions.SupportStoredProcedures` in options. -- Prefix stripping is automatic in result keys (`out_Result` becomes `Result`). -- Behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs` and XML examples in `DynamORM/DynamicDatabase.cs`. +- Behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs`. +- XML examples also appear in `DynamORM/DynamicDatabase.cs`.