Document typed stored procedure APIs
This commit is contained in:
@@ -1,30 +1,40 @@
|
||||
# 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
|
||||
|
||||
`DynamicProcedureInvoker` can treat the procedure "main result" as either:
|
||||
|
||||
- affected rows from `ExecuteNonQuery()`
|
||||
- provider return value parameter
|
||||
- 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)
|
||||
- `false`: invoker keeps `ExecuteNonQuery()` result
|
||||
|
||||
Why this matters:
|
||||
Why this exists:
|
||||
|
||||
- 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
|
||||
- some providers, including Oracle-style setups, do not behave the same way
|
||||
- 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
|
||||
var scalar = db.Procedures.sp_Exp_Scalar();
|
||||
@@ -41,7 +51,7 @@ Final command name is `dbo.reporting.MyProc`.
|
||||
|
||||
## 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`
|
||||
- `ret_` => `ParameterDirection.ReturnValue`
|
||||
@@ -57,15 +67,15 @@ dynamic res = db.Procedures.sp_Message_SetState(
|
||||
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
|
||||
dynamic res = db.Procedures.sp_Message_SetState(
|
||||
@@ -79,9 +89,9 @@ dynamic res = db.Procedures.sp_Message_SetState(
|
||||
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
|
||||
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
|
||||
dynamic res = db.Procedures.sp_Message_SetState(
|
||||
@@ -110,62 +120,66 @@ dynamic res = db.Procedures.sp_Message_SetState(
|
||||
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:
|
||||
|
||||
- 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:
|
||||
|
||||
- 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
|
||||
|
||||
## One generic type argument (`MyProc<TMain>(...)`)
|
||||
### One generic type argument
|
||||
|
||||
Main result type resolution:
|
||||
`db.Procedures.MyProc<TMain>(...)`
|
||||
|
||||
- `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`)
|
||||
Main result resolution:
|
||||
|
||||
- `TMain == IDataReader` => cached reader (`DynamicCachedReader`)
|
||||
- `TMain == DataTable` => `DataTable`
|
||||
- `TMain == IEnumerable<object>` / `List<object>` => list of dynamic rows
|
||||
- `TMain == IEnumerable<primitive>` / `List<primitive>` => first-column converted list
|
||||
- `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:
|
||||
|
||||
- if no out/ret params were requested: `TMain` result directly
|
||||
- if out/ret params exist: dynamic object containing main result + out params
|
||||
- if no out/ret params were requested: `TMain`
|
||||
- if out/ret params exist: dynamic payload containing main result plus out params
|
||||
|
||||
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
|
||||
- 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
|
||||
- main result and out params are packed into one payload
|
||||
- if `TOutModel` is mappable, payload is mapped to `TOutModel`
|
||||
- otherwise the result falls back to dynamic payload
|
||||
|
||||
## Result Key Names in Out Payload
|
||||
### Out payload key names
|
||||
|
||||
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)
|
||||
- main result is stored under procedure method name key
|
||||
- each out/both/ret param is stored under normalized parameter name
|
||||
|
||||
Example call:
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
- `sp_Message_SetState` (main result)
|
||||
- `sp_Message_SetState`
|
||||
- `message`
|
||||
- `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
|
||||
public class MessageSetStateResult
|
||||
public class SetMessageStatusArgs : IProcedureParameters<SetMessageStatusResult>
|
||||
{
|
||||
[Column("sp_Message_SetState")]
|
||||
public int MainResult { get; set; }
|
||||
[ProcedureParameter("status", Direction = ParameterDirection.ReturnValue, Order = 1)]
|
||||
public int Status { get; set; }
|
||||
|
||||
[Column("message")]
|
||||
public string Message { get; set; }
|
||||
[ProcedureParameter("id", Order = 2, DbType = DbType.String, Size = 64)]
|
||||
public string Id { get; set; }
|
||||
|
||||
[Column("code")]
|
||||
public int ReturnCode { get; set; }
|
||||
[ProcedureParameter("result", Direction = ParameterDirection.Output, Order = 3, DbType = DbType.Int32)]
|
||||
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.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
## Pattern 1: Main scalar only
|
||||
Then:
|
||||
|
||||
```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
|
||||
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);
|
||||
[Procedure(Name = "sp_set_message_status", Owner = "dbo")]
|
||||
public class SetMessageStatusProcedure : Procedure<SetMessageStatusArgs>
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 3: Procedure returning dataset as table
|
||||
This enables:
|
||||
|
||||
```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
|
||||
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
|
||||
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;
|
||||
var res1 = db.Procedures.Exec<SetMessageStatusProcedure, SetMessageStatusResult>(args);
|
||||
var res2 = db.Procedure<SetMessageStatusProcedure, SetMessageStatusResult>(args);
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
- 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`.
|
||||
- output value is truncated or null:
|
||||
- define explicit schema with `ProcedureParameterAttribute`, `DynamicSchemaColumn`, or `DynamicColumn`
|
||||
- unexpected return object shape:
|
||||
- check whether out/ret/inputoutput parameters are present
|
||||
- old dynamic invocation changes shape when out params are present
|
||||
- typed descriptor rejects arguments:
|
||||
- verify the argument object type matches the descriptor's `TArgs`
|
||||
- mapped result is incomplete:
|
||||
- verify payload keys match `ColumnAttribute` names
|
||||
- verify `ProcedureResultAttribute` indexes match actual reader result-set order
|
||||
- procedure fails on non-SQL Server provider:
|
||||
- disable `SupportStoredProceduresResult`
|
||||
- return status through explicit output parameters instead of SQL Server-style return-value semantics
|
||||
|
||||
Reference in New Issue
Block a user