Expand stored procedure docs with result-shape and output parameter guidance
This commit is contained in:
@@ -2,99 +2,239 @@
|
|||||||
|
|
||||||
Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled.
|
Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled.
|
||||||
|
|
||||||
## Basic Invocation
|
This page documents actual runtime behavior from `DynamicProcedureInvoker`.
|
||||||
|
|
||||||
|
## Invocation Basics
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var scalar = db.Procedures.sp_Exp_Scalar();
|
var scalar = db.Procedures.sp_Exp_Scalar();
|
||||||
var scalarTyped = db.Procedures.sp_Exp_Scalar<int>();
|
var scalarTyped = db.Procedures.sp_Exp_Scalar<int>();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Schema-Qualified Invocation
|
Schema-qualified naming is built through dynamic chaining:
|
||||||
|
|
||||||
Dynamic member chaining builds qualified names:
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var res = db.Procedures.dbo.reporting.MyProc();
|
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
|
- `out_` => `ParameterDirection.Output`
|
||||||
- `ret_` for return value
|
- `ret_` => `ParameterDirection.ReturnValue`
|
||||||
- `both_` for input/output
|
- `both_` => `ParameterDirection.InputOutput`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
dynamic res = db.Procedures.sp_Test_Scalar_In_Out(
|
dynamic res = db.Procedures.sp_Message_SetState(
|
||||||
inp: Guid.NewGuid(),
|
id: "abc-001",
|
||||||
out_outp: Guid.Empty,
|
both_state: "pending",
|
||||||
ret_Return: 0);
|
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
|
```csharp
|
||||||
var res = db.Procedures.sp_Exp_SomeInputAndOutput<string, ProcResult>(
|
dynamic res = db.Procedures.sp_Message_SetState(
|
||||||
Name: "G4g4r1n",
|
id: "abc-001",
|
||||||
out_Result: new DynamicSchemaColumn
|
out_message: new DynamicSchemaColumn
|
||||||
{
|
{
|
||||||
Name = "Result",
|
Name = "message",
|
||||||
Size = 256
|
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
|
```csharp
|
||||||
var res = db.Procedures.sp_WithInputOutput(
|
dynamic res = db.Procedures.sp_Message_SetState(
|
||||||
both_State: new DynamicColumn
|
both_state: new DynamicColumn
|
||||||
{
|
{
|
||||||
ColumnName = "State",
|
ColumnName = "state",
|
||||||
ParameterDirection = ParameterDirection.InputOutput,
|
ParameterDirection = ParameterDirection.InputOutput,
|
||||||
Value = "pending",
|
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:
|
Quickest form, but type/size inference is automatic.
|
||||||
|
|
||||||
- `T == IDataReader`: returns `CachedReader()` result.
|
|
||||||
- `T == DataTable`: materializes via `ToDataTable(...)`.
|
|
||||||
- `T == IEnumerable<object>`: dynamic row enumeration.
|
|
||||||
- `T == IEnumerable<primitive>`: converts first column of each row.
|
|
||||||
- `T == IEnumerable<class>`: maps rows via mapper cache.
|
|
||||||
- `T == class`: maps structured result to a class.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
IDataReader rdr = db.Procedures.MyProc<IDataReader>();
|
dynamic res = db.Procedures.sp_Message_SetState(
|
||||||
DataTable table = db.Procedures.MyProc<DataTable>();
|
id: "abc-001",
|
||||||
List<int> ids = db.Procedures.MyProc<List<int>>();
|
out_message: "",
|
||||||
List<User> users = db.Procedures.MyProc<List<User>>();
|
ret_code: 0);
|
||||||
User user = db.Procedures.MyProc<User>();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
`DynamicProcedureInvoker` chooses result shape from generic type arguments.
|
||||||
- output parameters
|
|
||||||
- return value
|
|
||||||
|
|
||||||
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<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, or enable provider option `SupportStoredProceduresResult`
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Enable `DynamicDatabaseOptions.SupportStoredProcedures` in options.
|
- Behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs`.
|
||||||
- Prefix stripping is automatic in result keys (`out_Result` becomes `Result`).
|
- XML examples also appear in `DynamORM/DynamicDatabase.cs`.
|
||||||
- Behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs` and XML examples in `DynamORM/DynamicDatabase.cs`.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user