Files
DynamORM/docs/stored-procedures.md

265 lines
7.6 KiB
Markdown

# 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`.