# 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(args)` or `db.Procedure(args)` 4. strongly typed direct calls through `db.Procedures.Exec(args)` or `db.Procedure(args)` 5. typed handle calls through `db.Procedures.Typed().Exec(args)` or `db.TypedProcedure().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(); ``` 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(...)` Main result resolution: - `TMain == IDataReader` => cached reader (`DynamicCachedReader`) - `TMain == DataTable` => `DataTable` - `TMain == IEnumerable` / `List` => list of dynamic rows - `TMain == IEnumerable` / `List` => first-column converted list - `TMain == IEnumerable` / `List` => 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(...)` 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( 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 { [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` ## 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 { } ``` This enables: ```csharp var res1 = db.Procedures.Exec(args); var res2 = db.Procedure(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` or `Procedure` - `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 { } ``` Calls: ```csharp var res1 = db.Procedures.Exec(args); var res2 = db.Procedure(args); ``` Why this needs two generic arguments: - C# cannot infer a method return type from `TProcedure : IProcedureDescriptor` 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().Exec(args); var res2 = db.TypedProcedure().Exec(args); ``` There is also a non-result variant: ```csharp var res = db.TypedProcedure().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 Codes { get; set; } [ProcedureResult(2, ColumnName = "State")] public int[] States { get; set; } [ProcedureResult(3)] public UserRow User { get; set; } [ProcedureResult(4)] public List 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 Codes { get; } = new List(); public List States { get; } = new List(); 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` 2. declared result from `IProcedureParameters` 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(...)` or `Procedure(...)` ### 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