504 lines
15 KiB
Markdown
504 lines
15 KiB
Markdown
# Stored Procedures
|
|
|
|
Stored procedure support is available when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled.
|
|
|
|
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
|
|
|
|
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
|
|
|
|
Why this exists:
|
|
|
|
- SQL Server commonly supports procedure return values in this style
|
|
- 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 output parameters for status/result codes.
|
|
|
|
## 1. Old Dynamic Invocation
|
|
|
|
This is the original API:
|
|
|
|
```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 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`
|
|
- `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, so `out_message` becomes `message` in the returned payload.
|
|
|
|
## How to Specify Type, Length, Precision, or Scale for Output Parameters
|
|
|
|
This is the most common issue with the old dynamic API.
|
|
|
|
### Option A: `DynamicSchemaColumn`
|
|
|
|
Use this for output-only parameters when schema must be explicit.
|
|
|
|
```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`
|
|
|
|
Use this when the parameter is input/output and you need both value and schema.
|
|
|
|
```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 with prefixed argument name
|
|
|
|
Fastest form, but type/size inference is automatic.
|
|
|
|
```csharp
|
|
dynamic res = db.Procedures.sp_Message_SetState(
|
|
id: "abc-001",
|
|
out_message: "",
|
|
ret_code: 0);
|
|
```
|
|
|
|
Use `DynamicSchemaColumn` or `DynamicColumn` whenever output schema must be controlled.
|
|
|
|
## Result Shape Matrix for Old Dynamic Invocation
|
|
|
|
### No generic type arguments
|
|
|
|
`db.Procedures.MyProc(...)`
|
|
|
|
Main result path:
|
|
|
|
- procedure executes via `ExecuteNonQuery()`
|
|
- 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/both params were requested: scalar main result
|
|
- if any out/ret/both params were requested: dynamic object with keys
|
|
|
|
### One generic type argument
|
|
|
|
`db.Procedures.MyProc<TMain>(...)`
|
|
|
|
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`
|
|
- if out/ret params exist: dynamic payload containing main result plus out params
|
|
|
|
Important nuance:
|
|
|
|
- with out params and only one generic type, the final result is payload-shaped, not raw `TMain`
|
|
|
|
### Two generic type arguments
|
|
|
|
`db.Procedures.MyProc<TMain, TOutModel>(...)`
|
|
|
|
This is the original preferred pattern when out params are involved.
|
|
|
|
- `TMain` is resolved as above
|
|
- 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
|
|
|
|
### Out payload key names
|
|
|
|
When out payload is used:
|
|
|
|
- main result is stored under procedure method name key
|
|
- each out/both/ret param is stored under normalized parameter name
|
|
|
|
Example:
|
|
|
|
```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`
|
|
- `message`
|
|
- `code`
|
|
|
|
## 2. Parameter Contract Object Invocation
|
|
|
|
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 SetMessageStatusArgs : IProcedureParameters<SetMessageStatusResult>
|
|
{
|
|
[ProcedureParameter("status", Direction = ParameterDirection.ReturnValue, Order = 1)]
|
|
public int Status { get; set; }
|
|
|
|
[ProcedureParameter("id", Order = 2, DbType = DbType.String, Size = 64)]
|
|
public string Id { 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; }
|
|
}
|
|
```
|
|
|
|
Then:
|
|
|
|
```csharp
|
|
var res = db.Procedures.sp_SetMessageStatus(new SetMessageStatusArgs
|
|
{
|
|
Id = "A-100",
|
|
Description = "seed"
|
|
});
|
|
```
|
|
|
|
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
|
|
[Procedure(Name = "sp_set_message_status", Owner = "dbo")]
|
|
public class SetMessageStatusProcedure : Procedure<SetMessageStatusArgs>
|
|
{
|
|
}
|
|
```
|
|
|
|
This enables:
|
|
|
|
```csharp
|
|
var res1 = db.Procedures.Exec<SetMessageStatusProcedure>(args);
|
|
var res2 = db.Procedure<SetMessageStatusProcedure>(args);
|
|
```
|
|
|
|
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
|
|
[Procedure(Name = "sp_set_message_status")]
|
|
public class SetMessageStatusProcedure : Procedure<SetMessageStatusArgs, SetMessageStatusResult>
|
|
{
|
|
}
|
|
```
|
|
|
|
Calls:
|
|
|
|
```csharp
|
|
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
|
|
|
|
- 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
|