Files
DynamORM/docs/stored-procedures.md

7.6 KiB

Stored Procedures

Stored procedure support is available through db.Procedures when DynamicDatabaseOptions.SupportStoredProcedures is enabled.

This page documents actual runtime behavior from DynamicProcedureInvoker.

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 (safer for providers that do not expose SQL Server-like return value behavior)

Why this matters:

  • 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

If procedures fail on non-SQL Server providers, first disable SupportStoredProceduresResult and rely on explicit out_ parameters for status/result codes.

Invocation Basics

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 dynamic out/ref is limited, parameter direction is encoded by argument name prefix:

  • out_ => ParameterDirection.Output
  • ret_ => ParameterDirection.ReturnValue
  • both_ => 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 (out_message -> message).

How to Specify Type/Length for Out Parameters

This is the most common pain point. You have 2 primary options.

Use this when you need explicit type, length, precision, or scale.

dynamic res = db.Procedures.sp_Message_SetState(
    id: "abc-001",
    out_message: new DynamicSchemaColumn
    {
        Name = "message",
        Type = DbType.String,
        Size = 1024,
    },
    ret_code: 0);

Use this when you need direction + value + schema in one object.

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 + name prefix

Quickest form, but type/size inference is automatic.

dynamic res = db.Procedures.sp_Message_SetState(
    id: "abc-001",
    out_message: "",
    ret_code: 0);

Use Option A/B whenever output size/type must be controlled.

Return Shape Matrix (What You Actually Get)

DynamicProcedureInvoker chooses result shape from generic type arguments.

No generic type arguments (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

Final returned object:

  • if no out/ret params were requested: scalar main result (int or return value)
  • if any out/ret/both params were requested: dynamic object with keys

One generic type argument (MyProc<TMain>(...))

Main result type resolution:

  • 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)

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

Important nuance:

  • when out params exist and only one generic type is provided, the result is a dynamic object, not bare TMain.

Two generic type arguments (MyProc<TMain, TOutModel>(...))

This is the 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

Result Key Names in Out Payload

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)

Example call:

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 (main result)
  • message
  • code

Preparing Result Classes Correctly

Use ColumnAttribute to map returned payload keys.

public class MessageSetStateResult
{
    [Column("sp_Message_SetState")]
    public int MainResult { get; set; }

    [Column("message")]
    public string Message { get; set; }

    [Column("code")]
    public int ReturnCode { get; set; }
}

If key names and property names already match, attributes are optional.

Common Patterns

Pattern 1: Main scalar only

int count = db.Procedures.sp_CountMessages<int>();

Pattern 2: Output params + mapped output model

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);

Pattern 3: Procedure returning dataset as table

DataTable dt = db.Procedures.sp_Message_GetBatch<DataTable>(batchId: 10);

Pattern 4: Procedure returning mapped collection

List<MessageRow> rows = db.Procedures.sp_Message_List<List<MessageRow>>(status: 1);

Pattern 5: Read output dynamically

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;

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.