Document typed stored procedure APIs
This commit is contained in:
@@ -79,3 +79,15 @@ table.Delete(code: "201");
|
|||||||
```
|
```
|
||||||
|
|
||||||
These forms are validated in `DynamORM.Tests/Modify/DynamicModificationTests.cs`.
|
These forms are validated in `DynamORM.Tests/Modify/DynamicModificationTests.cs`.
|
||||||
|
|
||||||
|
## Stored Procedures
|
||||||
|
|
||||||
|
Three common entry points:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var legacy = db.Procedures.sp_DoWork(id: 10, ret_code: 0);
|
||||||
|
var typed = db.Procedure<MyProcedure>(new MyProcedureArgs());
|
||||||
|
var typedResult = db.TypedProcedure<MyProcedure, MyProcedureResult>().Exec(new MyProcedureArgs());
|
||||||
|
```
|
||||||
|
|
||||||
|
Full details are documented in [Stored Procedures](stored-procedures.md).
|
||||||
|
|||||||
@@ -1,30 +1,40 @@
|
|||||||
# Stored Procedures
|
# Stored Procedures
|
||||||
|
|
||||||
Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled.
|
Stored procedure support is available when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled.
|
||||||
|
|
||||||
This page documents actual runtime behavior from `DynamicProcedureInvoker`.
|
There are now five practical ways to call procedures:
|
||||||
|
|
||||||
|
1. old dynamic invocation through `db.Procedures.SomeProc(...)`
|
||||||
|
2. parameter-contract object invocation through `db.Procedures.SomeProc(new Args())`
|
||||||
|
3. typed descriptor invocation through `db.Procedures.Exec<TProcedure>(args)` or `db.Procedure<TProcedure>(args)`
|
||||||
|
4. strongly typed direct calls through `db.Procedures.Exec<TProcedure, TResult>(args)` or `db.Procedure<TProcedure, TResult>(args)`
|
||||||
|
5. typed handle calls through `db.Procedures.Typed<TProcedure, TResult>().Exec(args)` or `db.TypedProcedure<TProcedure, TResult>().Exec(args)`
|
||||||
|
|
||||||
|
This page documents current runtime behavior from `DynamicProcedureInvoker`, `DynamicProcedureParameterBinder`, `DynamicProcedureResultBinder`, and the typed procedure descriptor APIs.
|
||||||
|
|
||||||
## `SupportStoredProceduresResult` and Provider Differences
|
## `SupportStoredProceduresResult` and Provider Differences
|
||||||
|
|
||||||
`DynamicProcedureInvoker` can treat the procedure "main result" as either:
|
`DynamicProcedureInvoker` can treat the procedure "main result" as either:
|
||||||
|
|
||||||
- affected rows from `ExecuteNonQuery()`
|
- affected rows from `ExecuteNonQuery()`
|
||||||
- provider return value parameter
|
- provider return-value parameter
|
||||||
|
|
||||||
This behavior is controlled by `DynamicDatabaseOptions.SupportStoredProceduresResult`.
|
This behavior is controlled by `DynamicDatabaseOptions.SupportStoredProceduresResult`.
|
||||||
|
|
||||||
- `true`: if a return-value parameter is present, invoker uses that value as main result
|
- `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)
|
- `false`: invoker keeps `ExecuteNonQuery()` result
|
||||||
|
|
||||||
Why this matters:
|
Why this exists:
|
||||||
|
|
||||||
- SQL Server commonly supports procedure return values in this style
|
- SQL Server commonly supports procedure return values in this style
|
||||||
- some providers (for example Oracle setups) do not behave the same way
|
- some providers, including Oracle-style setups, do not behave the same way
|
||||||
- forcing return-value extraction on such providers can cause runtime errors or invalid result handling
|
- forcing SQL Server-like return-value handling can cause runtime errors or invalid result handling on those providers
|
||||||
|
|
||||||
If procedures fail on non-SQL Server providers, first disable `SupportStoredProceduresResult` and rely on explicit `out_` parameters for status/result codes.
|
If procedures fail on non-SQL Server providers, first disable `SupportStoredProceduresResult` and rely on explicit output parameters for status/result codes.
|
||||||
|
|
||||||
## Invocation Basics
|
## 1. Old Dynamic Invocation
|
||||||
|
|
||||||
|
This is the original API:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var scalar = db.Procedures.sp_Exp_Scalar();
|
var scalar = db.Procedures.sp_Exp_Scalar();
|
||||||
@@ -41,7 +51,7 @@ Final command name is `dbo.reporting.MyProc`.
|
|||||||
|
|
||||||
## Parameter Direction Prefixes
|
## Parameter Direction Prefixes
|
||||||
|
|
||||||
Because dynamic `out/ref` is limited, parameter direction is encoded by argument name prefix:
|
Because C# dynamic invocation cannot use normal `out`/`ref` in this surface, parameter direction is encoded by argument name prefix:
|
||||||
|
|
||||||
- `out_` => `ParameterDirection.Output`
|
- `out_` => `ParameterDirection.Output`
|
||||||
- `ret_` => `ParameterDirection.ReturnValue`
|
- `ret_` => `ParameterDirection.ReturnValue`
|
||||||
@@ -57,15 +67,15 @@ dynamic res = db.Procedures.sp_Message_SetState(
|
|||||||
ret_code: 0);
|
ret_code: 0);
|
||||||
```
|
```
|
||||||
|
|
||||||
Prefix is removed from the exposed output key (`out_message` -> `message`).
|
Prefix is removed from the exposed output key, so `out_message` becomes `message` in the returned payload.
|
||||||
|
|
||||||
## How to Specify Type/Length for Out Parameters
|
## How to Specify Type, Length, Precision, or Scale for Output Parameters
|
||||||
|
|
||||||
This is the most common pain point. You have 2 primary options.
|
This is the most common issue with the old dynamic API.
|
||||||
|
|
||||||
## Option A: `DynamicSchemaColumn` (recommended for output-only)
|
### Option A: `DynamicSchemaColumn`
|
||||||
|
|
||||||
Use this when you need explicit type, length, precision, or scale.
|
Use this for output-only parameters when schema must be explicit.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
dynamic res = db.Procedures.sp_Message_SetState(
|
dynamic res = db.Procedures.sp_Message_SetState(
|
||||||
@@ -79,9 +89,9 @@ dynamic res = db.Procedures.sp_Message_SetState(
|
|||||||
ret_code: 0);
|
ret_code: 0);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Option B: `DynamicColumn` (recommended for input/output with value)
|
### Option B: `DynamicColumn`
|
||||||
|
|
||||||
Use this when you need direction + value + schema in one object.
|
Use this when the parameter is input/output and you need both value and schema.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
dynamic res = db.Procedures.sp_Message_SetState(
|
dynamic res = db.Procedures.sp_Message_SetState(
|
||||||
@@ -99,9 +109,9 @@ dynamic res = db.Procedures.sp_Message_SetState(
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Option C: Plain value + name prefix
|
### Option C: plain value with prefixed argument name
|
||||||
|
|
||||||
Quickest form, but type/size inference is automatic.
|
Fastest form, but type/size inference is automatic.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
dynamic res = db.Procedures.sp_Message_SetState(
|
dynamic res = db.Procedures.sp_Message_SetState(
|
||||||
@@ -110,62 +120,66 @@ dynamic res = db.Procedures.sp_Message_SetState(
|
|||||||
ret_code: 0);
|
ret_code: 0);
|
||||||
```
|
```
|
||||||
|
|
||||||
Use Option A/B whenever output size/type must be controlled.
|
Use `DynamicSchemaColumn` or `DynamicColumn` whenever output schema must be controlled.
|
||||||
|
|
||||||
## Return Shape Matrix (What You Actually Get)
|
## Result Shape Matrix for Old Dynamic Invocation
|
||||||
|
|
||||||
`DynamicProcedureInvoker` chooses result shape from generic type arguments.
|
### No generic type arguments
|
||||||
|
|
||||||
## No generic type arguments (`db.Procedures.MyProc(...)`)
|
`db.Procedures.MyProc(...)`
|
||||||
|
|
||||||
Main result path:
|
Main result path:
|
||||||
|
|
||||||
- procedure executes via `ExecuteNonQuery()`
|
- procedure executes via `ExecuteNonQuery()`
|
||||||
- if return-value support is enabled and return param is present, that value replaces affected-row count
|
- if return-value support is enabled and a return-value parameter is present, that value replaces affected-row count
|
||||||
|
|
||||||
Final returned object:
|
Final returned object:
|
||||||
|
|
||||||
- if no out/ret params were requested: scalar main result (`int` or return value)
|
- if no out/ret/both params were requested: scalar main result
|
||||||
- if any out/ret/both params were requested: dynamic object with keys
|
- if any out/ret/both params were requested: dynamic object with keys
|
||||||
|
|
||||||
## One generic type argument (`MyProc<TMain>(...)`)
|
### One generic type argument
|
||||||
|
|
||||||
Main result type resolution:
|
`db.Procedures.MyProc<TMain>(...)`
|
||||||
|
|
||||||
- `TMain == IDataReader` => returns cached reader (`DynamicCachedReader`)
|
Main result resolution:
|
||||||
- `TMain == DataTable` => returns `DataTable`
|
|
||||||
- `TMain == List<object>` or `IEnumerable<object>` => list of dynamic rows
|
- `TMain == IDataReader` => cached reader (`DynamicCachedReader`)
|
||||||
- `TMain == List<primitive/string>` => converted first-column list
|
- `TMain == DataTable` => `DataTable`
|
||||||
- `TMain == List<complex>` => mapped list
|
- `TMain == IEnumerable<object>` / `List<object>` => list of dynamic rows
|
||||||
- `TMain == primitive/string` => scalar converted value
|
- `TMain == IEnumerable<primitive>` / `List<primitive>` => first-column converted list
|
||||||
- `TMain == complex class` => first row mapped to class (or `null`)
|
- `TMain == IEnumerable<complex>` / `List<complex>` => mapped list
|
||||||
|
- `TMain == primitive/string/Guid` => converted scalar
|
||||||
|
- `TMain == complex class` => first row mapped to class or `null`
|
||||||
|
|
||||||
Final returned object:
|
Final returned object:
|
||||||
|
|
||||||
- if no out/ret params were requested: `TMain` result directly
|
- if no out/ret params were requested: `TMain`
|
||||||
- if out/ret params exist: dynamic object containing main result + out params
|
- if out/ret params exist: dynamic payload containing main result plus out params
|
||||||
|
|
||||||
Important nuance:
|
Important nuance:
|
||||||
|
|
||||||
- when out params exist and only one generic type is provided, the result is a dynamic object, not bare `TMain`.
|
- with out params and only one generic type, the final result is payload-shaped, not raw `TMain`
|
||||||
|
|
||||||
## Two generic type arguments (`MyProc<TMain, TOutModel>(...)`)
|
### Two generic type arguments
|
||||||
|
|
||||||
This is the preferred pattern when out params are involved.
|
`db.Procedures.MyProc<TMain, TOutModel>(...)`
|
||||||
|
|
||||||
|
This is the original preferred pattern when out params are involved.
|
||||||
|
|
||||||
- `TMain` is resolved as above
|
- `TMain` is resolved as above
|
||||||
- out/main values are packed into a dictionary-like dynamic payload
|
- main result and out params are packed into one payload
|
||||||
- if mapper for `TOutModel` exists, payload is mapped to `TOutModel`
|
- if `TOutModel` is mappable, payload is mapped to `TOutModel`
|
||||||
- otherwise fallback is dynamic object
|
- otherwise the result falls back to dynamic payload
|
||||||
|
|
||||||
## Result Key Names in Out Payload
|
### Out payload key names
|
||||||
|
|
||||||
When out payload is used:
|
When out payload is used:
|
||||||
|
|
||||||
- main result is stored under procedure method name key (`binder.Name`)
|
- main result is stored under procedure method name key
|
||||||
- each out/both/ret param is stored under normalized parameter name (without prefix)
|
- each out/both/ret param is stored under normalized parameter name
|
||||||
|
|
||||||
Example call:
|
Example:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var res = db.Procedures.sp_Message_SetState<int, MessageSetStateResult>(
|
var res = db.Procedures.sp_Message_SetState<int, MessageSetStateResult>(
|
||||||
@@ -176,89 +190,314 @@ var res = db.Procedures.sp_Message_SetState<int, MessageSetStateResult>(
|
|||||||
|
|
||||||
Expected payload keys before mapping:
|
Expected payload keys before mapping:
|
||||||
|
|
||||||
- `sp_Message_SetState` (main result)
|
- `sp_Message_SetState`
|
||||||
- `message`
|
- `message`
|
||||||
- `code`
|
- `code`
|
||||||
|
|
||||||
## Preparing Result Classes Correctly
|
## 2. Parameter Contract Object Invocation
|
||||||
|
|
||||||
Use `ColumnAttribute` to map returned payload keys.
|
This is the new object-contract path.
|
||||||
|
|
||||||
|
Instead of encoding output metadata into prefixed dynamic arguments, you pass a single object implementing `IProcedureParameters`.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public class MessageSetStateResult
|
public class SetMessageStatusArgs : IProcedureParameters<SetMessageStatusResult>
|
||||||
{
|
{
|
||||||
[Column("sp_Message_SetState")]
|
[ProcedureParameter("status", Direction = ParameterDirection.ReturnValue, Order = 1)]
|
||||||
public int MainResult { get; set; }
|
public int Status { get; set; }
|
||||||
|
|
||||||
[Column("message")]
|
[ProcedureParameter("id", Order = 2, DbType = DbType.String, Size = 64)]
|
||||||
public string Message { get; set; }
|
public string Id { get; set; }
|
||||||
|
|
||||||
[Column("code")]
|
[ProcedureParameter("result", Direction = ParameterDirection.Output, Order = 3, DbType = DbType.Int32)]
|
||||||
public int ReturnCode { get; set; }
|
public int Result { get; set; }
|
||||||
|
|
||||||
|
[ProcedureParameter("description", Direction = ParameterDirection.InputOutput, Order = 4, DbType = DbType.String, Size = 1024)]
|
||||||
|
public string Description { get; set; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If key names and property names already match, attributes are optional.
|
Then:
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
## Pattern 1: Main scalar only
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
int count = db.Procedures.sp_CountMessages<int>();
|
var res = db.Procedures.sp_SetMessageStatus(new SetMessageStatusArgs
|
||||||
|
{
|
||||||
|
Id = "A-100",
|
||||||
|
Description = "seed"
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pattern 2: Output params + mapped output model
|
Rules:
|
||||||
|
|
||||||
|
- the object must implement `IProcedureParameters`
|
||||||
|
- object mode activates only when exactly one argument is passed
|
||||||
|
- `ProcedureParameterAttribute` controls name, direction, order, type, size, precision, and scale
|
||||||
|
- `ColumnAttribute` can still provide fallback name/type/size metadata for compatibility
|
||||||
|
|
||||||
|
## Why use parameter contracts
|
||||||
|
|
||||||
|
Advantages:
|
||||||
|
|
||||||
|
- one argument object instead of prefixed parameter names
|
||||||
|
- output and return-value schema are explicit on the contract
|
||||||
|
- order is stable and documented in one place
|
||||||
|
- result type can be declared through `IProcedureParameters<TResult>`
|
||||||
|
|
||||||
|
## 3. Typed Procedure Descriptors
|
||||||
|
|
||||||
|
You can now describe a procedure with a class-level descriptor.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var res = db.Procedures.sp_Message_SetState<int, MessageSetStateResult>(
|
[Procedure(Name = "sp_set_message_status", Owner = "dbo")]
|
||||||
id: "abc-001",
|
public class SetMessageStatusProcedure : Procedure<SetMessageStatusArgs>
|
||||||
status: 2,
|
{
|
||||||
out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 },
|
}
|
||||||
ret_code: 0);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pattern 3: Procedure returning dataset as table
|
This enables:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
DataTable dt = db.Procedures.sp_Message_GetBatch<DataTable>(batchId: 10);
|
var res1 = db.Procedures.Exec<SetMessageStatusProcedure>(args);
|
||||||
|
var res2 = db.Procedure<SetMessageStatusProcedure>(args);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pattern 4: Procedure returning mapped collection
|
Descriptor rules:
|
||||||
|
|
||||||
|
- `ProcedureAttribute` is similar to `TableAttribute`
|
||||||
|
- `Name` controls procedure name
|
||||||
|
- `Owner` adds schema/owner prefix
|
||||||
|
- if `ProcedureAttribute` is omitted, descriptor class name is used as procedure name
|
||||||
|
- descriptor must inherit from `Procedure<TArgs>` or `Procedure<TArgs, TResult>`
|
||||||
|
- `TArgs` must implement `IProcedureParameters`
|
||||||
|
|
||||||
|
## 4. Strongly Typed Direct Calls
|
||||||
|
|
||||||
|
If the descriptor declares a result type, you can use the explicit strongly typed overloads.
|
||||||
|
|
||||||
|
Descriptor:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
List<MessageRow> rows = db.Procedures.sp_Message_List<List<MessageRow>>(status: 1);
|
[Procedure(Name = "sp_set_message_status")]
|
||||||
|
public class SetMessageStatusProcedure : Procedure<SetMessageStatusArgs, SetMessageStatusResult>
|
||||||
|
{
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pattern 5: Read output dynamically
|
Calls:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
dynamic res = db.Procedures.sp_Message_SetState(
|
var res1 = db.Procedures.Exec<SetMessageStatusProcedure, SetMessageStatusResult>(args);
|
||||||
id: "abc-001",
|
var res2 = db.Procedure<SetMessageStatusProcedure, SetMessageStatusResult>(args);
|
||||||
out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 },
|
|
||||||
ret_code: 0);
|
|
||||||
|
|
||||||
var message = (string)res.message;
|
|
||||||
var code = (int)res.code;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Why this needs two generic arguments:
|
||||||
|
|
||||||
|
- C# cannot infer a method return type from `TProcedure : IProcedureDescriptor<TResult>` alone
|
||||||
|
- the descriptor constraint is enough for validation, but not enough for single-generic return typing
|
||||||
|
|
||||||
|
So the two-generic overload is the direct strongly typed API.
|
||||||
|
|
||||||
|
## 5. Typed Handle Calls
|
||||||
|
|
||||||
|
To avoid repeating the result generic on the `Exec(...)` step, you can create a typed execution handle.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var res1 = db.Procedures.Typed<SetMessageStatusProcedure, SetMessageStatusResult>().Exec(args);
|
||||||
|
var res2 = db.TypedProcedure<SetMessageStatusProcedure, SetMessageStatusResult>().Exec(args);
|
||||||
|
```
|
||||||
|
|
||||||
|
There is also a non-result variant:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var res = db.TypedProcedure<SetMessageStatusProcedure>().Exec(args);
|
||||||
|
```
|
||||||
|
|
||||||
|
This API exists because it gives one setup step with descriptor/result typing, then a normal strongly typed `Exec(...)` call.
|
||||||
|
|
||||||
|
## Declaring Typed Procedure Results
|
||||||
|
|
||||||
|
There are three supported result declaration patterns.
|
||||||
|
|
||||||
|
### Pattern A: payload mapping with `ColumnAttribute`
|
||||||
|
|
||||||
|
This is the classic approach.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SetMessageStatusResult
|
||||||
|
{
|
||||||
|
[Column("sp_set_message_status")]
|
||||||
|
public int MainResult { get; set; }
|
||||||
|
|
||||||
|
[Column("result")]
|
||||||
|
public int Result { get; set; }
|
||||||
|
|
||||||
|
[Column("description")]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[Column("status")]
|
||||||
|
public int Status { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern B: main result through `ProcedureResultAttribute`
|
||||||
|
|
||||||
|
This is now also supported.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SetMessageStatusResult
|
||||||
|
{
|
||||||
|
[ProcedureResult]
|
||||||
|
public int MainResult { get; set; }
|
||||||
|
|
||||||
|
[Column("result")]
|
||||||
|
public int Result { get; set; }
|
||||||
|
|
||||||
|
[Column("description")]
|
||||||
|
public string Description { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Equivalent explicit form:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ProcedureResult(-1)]
|
||||||
|
public int MainResult { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `[Column("ProcedureName")]` is still supported and unchanged
|
||||||
|
- `[ProcedureResult]` / `[ProcedureResult(-1)]` is additive, not a replacement
|
||||||
|
- only one main-result member is allowed on a result type
|
||||||
|
|
||||||
|
### Pattern C: multiple reader result sets through `ProcedureResultAttribute`
|
||||||
|
|
||||||
|
A typed result can bind individual reader result sets to specific members.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class BatchResult
|
||||||
|
{
|
||||||
|
[ProcedureResult]
|
||||||
|
public int MainResult { get; set; }
|
||||||
|
|
||||||
|
[Column("status")]
|
||||||
|
public int Status { get; set; }
|
||||||
|
|
||||||
|
[ProcedureResult(0, ColumnName = "Code")]
|
||||||
|
public string FirstCode { get; set; }
|
||||||
|
|
||||||
|
[ProcedureResult(1, ColumnName = "Code")]
|
||||||
|
public List<string> Codes { get; set; }
|
||||||
|
|
||||||
|
[ProcedureResult(2, ColumnName = "State")]
|
||||||
|
public int[] States { get; set; }
|
||||||
|
|
||||||
|
[ProcedureResult(3)]
|
||||||
|
public UserRow User { get; set; }
|
||||||
|
|
||||||
|
[ProcedureResult(4)]
|
||||||
|
public List<UserRow> Users { get; set; }
|
||||||
|
|
||||||
|
[ProcedureResult(5, Name = "codes_table")]
|
||||||
|
public DataTable CodesTable { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported member shapes:
|
||||||
|
|
||||||
|
- scalar/simple value
|
||||||
|
- list or array of simple values
|
||||||
|
- mapped complex object
|
||||||
|
- list or array of mapped complex objects
|
||||||
|
- `DataTable`
|
||||||
|
- public property or public field
|
||||||
|
|
||||||
|
For scalar and simple-list members:
|
||||||
|
|
||||||
|
- first column is used by default
|
||||||
|
- or `ColumnName` can select a specific column
|
||||||
|
|
||||||
|
For `DataTable` members:
|
||||||
|
|
||||||
|
- `Name` becomes the table name
|
||||||
|
|
||||||
|
## Custom Multi-Result Reader Logic
|
||||||
|
|
||||||
|
If the declarative `ProcedureResultAttribute` model is not enough, the result type can implement `IProcedureResultReader`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class BatchResult : IProcedureResultReader
|
||||||
|
{
|
||||||
|
public List<string> Codes { get; } = new List<string>();
|
||||||
|
public List<int> States { get; } = new List<int>();
|
||||||
|
|
||||||
|
public void ReadResults(IDataReader reader)
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
Codes.Add(reader.GetString(0));
|
||||||
|
|
||||||
|
if (reader.NextResult())
|
||||||
|
while (reader.Read())
|
||||||
|
States.Add(reader.GetInt32(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This remains the escape hatch for complex provider-specific or custom reading logic.
|
||||||
|
|
||||||
|
## Which Result Type Wins
|
||||||
|
|
||||||
|
Result type precedence for the new APIs:
|
||||||
|
|
||||||
|
1. explicit descriptor result from `Procedure<TArgs, TResult>`
|
||||||
|
2. declared result from `IProcedureParameters<TResult>` on the argument contract
|
||||||
|
3. old generic result arguments from dynamic invocation
|
||||||
|
4. fallback dynamic payload/scalar behavior
|
||||||
|
|
||||||
|
That allows descriptor-level result typing to override argument-level declarations when needed.
|
||||||
|
|
||||||
|
## Recommended Usage Patterns
|
||||||
|
|
||||||
|
### Keep using old dynamic invocation when
|
||||||
|
|
||||||
|
- the procedure is simple
|
||||||
|
- you want minimal ceremony
|
||||||
|
- outputs are few and dynamic payload is acceptable
|
||||||
|
|
||||||
|
### Use parameter contracts when
|
||||||
|
|
||||||
|
- output metadata is important
|
||||||
|
- procedures have many parameters
|
||||||
|
- you want stable parameter ordering and schema definition
|
||||||
|
|
||||||
|
### Use typed descriptors when
|
||||||
|
|
||||||
|
- the procedure is reused across the codebase
|
||||||
|
- you want one named procedure contract per stored procedure
|
||||||
|
- you want static procedure-name metadata instead of string-based calls
|
||||||
|
|
||||||
|
### Use strongly typed direct calls when
|
||||||
|
|
||||||
|
- you want a single call expression returning `TResult`
|
||||||
|
- you are fine with `Exec<TProcedure, TResult>(...)` or `Procedure<TProcedure, TResult>(...)`
|
||||||
|
|
||||||
|
### Use typed handles when
|
||||||
|
|
||||||
|
- you want the strongest typing without relying on dynamic dispatch
|
||||||
|
- you want to configure the descriptor/result type once and then call `.Exec(...)`
|
||||||
|
|
||||||
## Troubleshooting Checklist
|
## Troubleshooting Checklist
|
||||||
|
|
||||||
- Out value is truncated or null:
|
- output value is truncated or null:
|
||||||
- define output schema explicitly with `DynamicSchemaColumn` (type + size)
|
- define explicit schema with `ProcedureParameterAttribute`, `DynamicSchemaColumn`, or `DynamicColumn`
|
||||||
- Unexpected return object shape:
|
- unexpected return object shape:
|
||||||
- check whether any out/ret/both parameter was passed
|
- check whether out/ret/inputoutput parameters are present
|
||||||
- if yes, expect out payload object unless you used 2-generic mapping variant
|
- old dynamic invocation changes shape when out params are present
|
||||||
- Mapping to class fails silently (dynamic fallback):
|
- typed descriptor rejects arguments:
|
||||||
- ensure output model is mappable and keys match columns/properties
|
- verify the argument object type matches the descriptor's `TArgs`
|
||||||
- Return value not appearing:
|
- mapped result is incomplete:
|
||||||
- ensure `ret_` parameter is supplied
|
- verify payload keys match `ColumnAttribute` names
|
||||||
- ensure provider option `SupportStoredProceduresResult` matches your DB behavior
|
- verify `ProcedureResultAttribute` indexes match actual reader result-set order
|
||||||
- Procedure call errors on non-SQL Server providers:
|
- procedure fails on non-SQL Server provider:
|
||||||
- set `SupportStoredProceduresResult = false`
|
- disable `SupportStoredProceduresResult`
|
||||||
- return status via explicit `out_` parameters instead of return-value semantics
|
- return status through explicit output parameters instead of SQL Server-style return-value semantics
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs`.
|
|
||||||
- XML examples also appear in `DynamORM/DynamicDatabase.cs`.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user