Learnixo
Back to blog
AI Systemsintermediate

Dapper Stored Procedures — Calling and Mapping Stored Procedure Results

Call SQL Server stored procedures with Dapper: CommandType.StoredProcedure, input/output parameters, return values, multi-result-set stored procedures, and when stored procedures make sense versus inline SQL.

Asma Hafeez KhanMay 16, 20264 min read
DapperStored ProceduresSQL ServerASP.NET Core.NETDatabase
Share:𝕏

Calling a Stored Procedure

C#
// Use CommandType.StoredProcedure — Dapper sends EXEC sp_name
var patients = await connection.QueryAsync<PatientDto>(
    "sp_GetWardPatients",
    new { WardId = wardId, IncludeArchived = false },
    commandType: CommandType.StoredProcedure);

// Without CommandType.StoredProcedure, Dapper sends the string as raw SQL.
// CommandType.StoredProcedure generates: EXEC sp_GetWardPatients @WardId = @p0, ...

// With timeout
var result = await connection.QueryFirstOrDefaultAsync<PatientDto>(
    "sp_GetPatientByMrn",
    new { Mrn = mrn },
    commandType:    CommandType.StoredProcedure,
    commandTimeout: 30);

Output Parameters and Return Values

C#
// Stored procedure with INPUT + OUTPUT parameters + RETURN value
// CREATE PROCEDURE sp_AdmitPatient
//   @PatientId    UNIQUEIDENTIFIER,
//   @WardId       UNIQUEIDENTIFIER,
//   @NewAdmissionId UNIQUEIDENTIFIER OUTPUT
// AS ...

var parameters = new DynamicParameters();
parameters.Add("@PatientId",      patientId,     DbType.Guid);
parameters.Add("@WardId",         wardId,        DbType.Guid);
parameters.Add("@NewAdmissionId", dbType: DbType.Guid, direction: ParameterDirection.Output);
parameters.Add("@ReturnValue",    dbType: DbType.Int32, direction: ParameterDirection.ReturnValue);

await connection.ExecuteAsync(
    "sp_AdmitPatient",
    parameters,
    commandType: CommandType.StoredProcedure);

var admissionId = parameters.Get<Guid>("@NewAdmissionId");
var returnCode  = parameters.Get<int>("@ReturnValue");
// returnCode: 0 = success, 1 = ward full, 2 = patient already admitted

Multi-Result-Set Stored Procedure

C#
// Stored procedure that returns multiple result sets
// sp_GetPatientFullRecord returns:
//   Result 1: patient demographics
//   Result 2: active prescriptions
//   Result 3: recent lab results

using var multi = await connection.QueryMultipleAsync(
    "sp_GetPatientFullRecord",
    new { PatientId = patientId },
    commandType: CommandType.StoredProcedure);

var patient       = await multi.ReadFirstOrDefaultAsync<PatientDto>();
var prescriptions = (await multi.ReadAsync<PrescriptionDto>()).AsList();
var labResults    = (await multi.ReadAsync<LabResultDto>()).AsList();

return new PatientFullRecordDto(patient, prescriptions, labResults);

Stored Procedure with Table-Valued Parameter

C#
// SQL: CREATE TYPE dbo.ObservationList AS TABLE (
//   PatientId  UNIQUEIDENTIFIER,
//   Type       NVARCHAR(50),
//   Value      DECIMAL(10,4),
//   RecordedAt DATETIME2)

// C#: build DataTable matching the TVP schema
var tvp = new DataTable();
tvp.Columns.Add("PatientId",  typeof(Guid));
tvp.Columns.Add("Type",       typeof(string));
tvp.Columns.Add("Value",      typeof(decimal));
tvp.Columns.Add("RecordedAt", typeof(DateTime));

foreach (var obs in observations)
    tvp.Rows.Add(obs.PatientId, obs.Type, obs.Value, obs.RecordedAt);

var parameters = new DynamicParameters();
parameters.Add("@Observations", tvp.AsTableValuedParameter("dbo.ObservationList"));

await connection.ExecuteAsync(
    "sp_BulkInsertObservations",
    parameters,
    commandType: CommandType.StoredProcedure);

When Stored Procedures Make Sense

Stored procedures are appropriate when:
  ✓ Complex business logic already exists in SQL and the team owns the database
  ✓ Security: grant EXECUTE on procs, deny direct table SELECT/INSERT access
  ✓ Performance-critical queries where you need the exact execution plan
  ✓ Batch operations with MERGE, temp tables, or set-based logic
  ✓ Legacy systems where the database is the primary deployment artifact

Stored procedures create friction when:
  ✗ The application team does not own the database (DBA bottleneck)
  ✗ Schema evolution requires coordinated application + proc changes
  ✗ Unit testing business logic requires a database (no mocking)
  ✗ Code review for SQL procs is in a separate system from application code

In modern .NET systems: prefer inline parameterized SQL in repositories
  unless the organization has established stored procedure governance.

Wrapping Stored Procedure in Repository

C#
public interface IWarfarinRepository
{
    Task<WarfarinDoseSuggestionDto?> GetDoseSuggestionAsync(
        Guid patientId, CancellationToken ct);
}

public sealed class WarfarinRepository : IWarfarinRepository
{
    private readonly ISqlConnectionFactory _factory;

    public async Task<WarfarinDoseSuggestionDto?> GetDoseSuggestionAsync(
        Guid patientId, CancellationToken ct)
    {
        using var connection = _factory.CreateConnection();

        var parameters = new DynamicParameters();
        parameters.Add("@PatientId", patientId, DbType.Guid);
        parameters.Add("@SuggestedDose", dbType: DbType.Decimal, direction: ParameterDirection.Output, precision: 10, scale: 4);
        parameters.Add("@Reason",        dbType: DbType.String,  direction: ParameterDirection.Output, size: 500);
        parameters.Add("@ReturnValue",   dbType: DbType.Int32,   direction: ParameterDirection.ReturnValue);

        await connection.ExecuteAsync(
            "sp_CalculateWarfarinDoseSuggestion",
            parameters,
            commandType: CommandType.StoredProcedure);

        if (parameters.Get<int>("@ReturnValue") != 0)
            return null;

        return new WarfarinDoseSuggestionDto(
            parameters.Get<decimal>("@SuggestedDose"),
            parameters.Get<string>("@Reason") ?? string.Empty);
    }
}

Production issue I've seen: A team used a stored procedure for their patient search that accepted a search term as a VARCHAR parameter. Inside the stored procedure, the developer built the SQL with string concatenation: SET @Sql = 'SELECT * FROM patients WHERE name LIKE ''' + @SearchTerm + ''''. The stored procedure had a SQL injection vulnerability even though Dapper was used — because the stored procedure itself used dynamic SQL without parameterization. Using sp_executesql with parameterized dynamic SQL inside the stored procedure would have prevented this.


Key Takeaway

Use CommandType.StoredProcedure to call stored procedures — never build the EXEC string manually. Use DynamicParameters with ParameterDirection.Output for output parameters and ParameterDirection.ReturnValue for return codes. Use QueryMultiple for stored procedures that return multiple result sets. Validate that stored procedures with dynamic internal SQL use sp_executesql with parameters — SQL injection inside a stored procedure is still SQL injection.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.