15 KiB
Stored Procedures
Stored procedure support is available when DynamicDatabaseOptions.SupportStoredProcedures is enabled.
There are now five practical ways to call procedures:
- old dynamic invocation through
db.Procedures.SomeProc(...) - parameter-contract object invocation through
db.Procedures.SomeProc(new Args()) - typed descriptor invocation through
db.Procedures.Exec<TProcedure>(args)ordb.Procedure<TProcedure>(args) - strongly typed direct calls through
db.Procedures.Exec<TProcedure, TResult>(args)ordb.Procedure<TProcedure, TResult>(args) - typed handle calls through
db.Procedures.Typed<TProcedure, TResult>().Exec(args)ordb.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 resultfalse: invoker keepsExecuteNonQuery()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:
var scalar = db.Procedures.sp_Exp_Scalar();
var scalarTyped = db.Procedures.sp_Exp_Scalar<int>();
Schema-qualified naming is built through dynamic chaining:
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.Outputret_=>ParameterDirection.ReturnValueboth_=>ParameterDirection.InputOutput
Example:
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.
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.
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.
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=>DataTableTMain == IEnumerable<object>/List<object>=> list of dynamic rowsTMain == IEnumerable<primitive>/List<primitive>=> first-column converted listTMain == IEnumerable<complex>/List<complex>=> mapped listTMain == primitive/string/Guid=> converted scalarTMain == complex class=> first row mapped to class ornull
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.
TMainis resolved as above- main result and out params are packed into one payload
- if
TOutModelis mappable, payload is mapped toTOutModel - 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:
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_SetStatemessagecode
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.
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:
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
ProcedureParameterAttributecontrols name, direction, order, type, size, precision, and scaleColumnAttributecan 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.
[Procedure(Name = "sp_set_message_status", Owner = "dbo")]
public class SetMessageStatusProcedure : Procedure<SetMessageStatusArgs>
{
}
This enables:
var res1 = db.Procedures.Exec<SetMessageStatusProcedure>(args);
var res2 = db.Procedure<SetMessageStatusProcedure>(args);
Descriptor rules:
ProcedureAttributeis similar toTableAttributeNamecontrols procedure nameOwneradds schema/owner prefix- if
ProcedureAttributeis omitted, descriptor class name is used as procedure name - descriptor must inherit from
Procedure<TArgs>orProcedure<TArgs, TResult> TArgsmust implementIProcedureParameters
4. Strongly Typed Direct Calls
If the descriptor declares a result type, you can use the explicit strongly typed overloads.
Descriptor:
[Procedure(Name = "sp_set_message_status")]
public class SetMessageStatusProcedure : Procedure<SetMessageStatusArgs, SetMessageStatusResult>
{
}
Calls:
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.
var res1 = db.Procedures.Typed<SetMessageStatusProcedure, SetMessageStatusResult>().Exec(args);
var res2 = db.TypedProcedure<SetMessageStatusProcedure, SetMessageStatusResult>().Exec(args);
There is also a non-result variant:
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.
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.
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:
[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.
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
ColumnNamecan select a specific column
For DataTable members:
Namebecomes the table name
Custom Multi-Result Reader Logic
If the declarative ProcedureResultAttribute model is not enough, the result type can implement IProcedureResultReader.
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:
- explicit descriptor result from
Procedure<TArgs, TResult> - declared result from
IProcedureParameters<TResult>on the argument contract - old generic result arguments from dynamic invocation
- 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>(...)orProcedure<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, orDynamicColumn
- define explicit schema with
- 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
- verify the argument object type matches the descriptor's
- mapped result is incomplete:
- verify payload keys match
ColumnAttributenames - verify
ProcedureResultAttributeindexes match actual reader result-set order
- verify payload keys match
- procedure fails on non-SQL Server provider:
- disable
SupportStoredProceduresResult - return status through explicit output parameters instead of SQL Server-style return-value semantics
- disable