Módulo 2 · SQL Server / Procedures — Capítulo 13

Integração com C#

As três formas mais comuns de chamar uma procedure a partir do C#: ADO.NET puro, Dapper e Entity Framework Core — com prós e contras de cada uma.

1. Três camadas, um mesmo objetivo

Toda procedure deste curso, no fim das contas, precisa ser chamada de algum lugar na aplicação C#. Existem três níveis de abstração comuns, do mais explícito ao mais automático:

ADO.NETDapperEntity Framework Core
Nível de abstraçãoBaixo — você escreve tudoMédio — micro-ORMAlto — ORM completo
Mapeamento objeto ↔ linhaManualAutomático (reflection)Automático (rastreado)
Melhor paraControle total, performance críticaMaioria dos casos do dia a diaSistemas já modelados com EF, CRUD simples

2. ADO.NET puro

A base sobre a qual tudo o resto é construído. Você já viu fragmentos disso nos capítulos anteriores — aqui está o padrão completo, incluindo tratamento assíncrono:

C# ADO.NET — assíncrono, com mapeamento manual
public async Task<List<Cliente>> ListarClientesAsync(string? nome, bool? ativo)
{
    var clientes = new List<Cliente>();

    await using var conexao = new SqlConnection(_connectionString);
    await using var comando = new SqlCommand("dbo.usp_ClienteListar", conexao);
    comando.CommandType = CommandType.StoredProcedure;

    comando.Parameters.Add("@Nome", SqlDbType.NVarChar, 100).Value = (object?)nome ?? DBNull.Value;
    comando.Parameters.Add("@Ativo", SqlDbType.Bit).Value = (object?)ativo ?? DBNull.Value;

    await conexao.OpenAsync();
    await using var leitor = await comando.ExecuteReaderAsync();

    while (await leitor.ReadAsync())
    {
        clientes.Add(new Cliente
        {
            Id    = leitor.GetInt32(leitor.GetOrdinal("Id")),
            Nome  = leitor.GetString(leitor.GetOrdinal("Nome")),
            Email = leitor.GetString(leitor.GetOrdinal("Email")),
        });
    }

    return clientes;
}
Nota

(object?)nome ?? DBNull.Value é o detalhe que mais pega quem começa: um null do C# não vira automaticamente NULL do SQL — é preciso converter explicitamente para DBNull.Value.

3. Dapper — o meio-termo mais usado na prática

Dapper mapeia o resultado direto para objetos, eliminando o loop manual de leitura, mantendo o controle explícito sobre qual SQL/procedure roda:

C# o mesmo método, com Dapper
public async Task<IEnumerable<Cliente>> ListarClientesAsync(string? nome, bool? ativo)
{
    await using var conexao = new SqlConnection(_connectionString);

    return await conexao.QueryAsync<Cliente>(
        "dbo.usp_ClienteListar",
        new { Nome = nome, Ativo = ativo },
        commandType: CommandType.StoredProcedure);
}

Dapper casa o nome de cada coluna do result set com uma propriedade de mesmo nome na classe Cliente automaticamente — reduzindo o método inteiro a uma chamada. Para procedures com parâmetro OUTPUT:

C# Dapper com parâmetro OUTPUT
var parametros = new DynamicParameters();
parametros.Add("@Nome", nome);
parametros.Add("@Email", email);
parametros.Add("@NovoId", dbType: DbType.Int32, direction: ParameterDirection.Output);

await conexao.ExecuteAsync(
    "dbo.usp_ClienteInserir",
    parametros,
    commandType: CommandType.StoredProcedure);

int novoId = parametros.Get<int>("@NovoId");

4. Entity Framework Core

EF Core é pensado em torno de DbSet<T> e LINQ, mas também chama procedures diretamente quando necessário — útil sobretudo para escrita (INSERT/UPDATE/DELETE) e para leituras que não mapeiam bem para uma entidade rastreada:

C# FromSqlInterpolated (leitura) e ExecuteSqlInterpolatedAsync (escrita)
// Leitura — mapeia para uma entidade/DTO conhecido pelo EF
var clientes = await _contexto.Clientes
    .FromSqlInterpolated($"EXEC dbo.usp_ClienteListar @Nome = {nome}, @Ativo = {ativo}")
    .ToListAsync();

// Escrita — não devolve entidade, só executa
int linhasAfetadas = await _contexto.Database
    .ExecuteSqlInterpolatedAsync($"EXEC dbo.usp_ClienteAtualizar @Id = {id}, @Nome = {nome}, @Email = {email}");
Dica de segurança

Use sempre FromSqlInterpolated/ExecuteSqlInterpolatedAsync (com $"..."), nunca FromSqlRaw concatenando strings manualmente. A versão interpolada faz o EF Core tratar cada {variavel} como parâmetro real — a mesma proteção contra SQL injection do capítulo 10, só que do lado do C#.

5. Qual escolher

CenárioRecomendação
Projeto novo, time pequeno, quer produtividadeDapper
Já usa EF Core para o resto do domínioEF Core com FromSql/ExecuteSql para os casos que fogem do LINQ
Biblioteca/serviço de infraestrutura, performance crítica, controle fino de conexãoADO.NET puro

6. Onde isso te leva

Com as três formas de integração cobertas, o último capítulo deste módulo junta tudo — do modelo de dados até a chamada em C# — num projeto prático fim a fim.

📌 Resumo do capítulo

  • ADO.NET dá controle total, mas exige mapeamento manual de cada coluna.
  • Dapper mapeia automaticamente por nome de propriedade, mantendo o SQL/procedure explícito — bom equilíbrio para a maioria dos projetos.
  • EF Core chama procedures via FromSqlInterpolated (leitura) e ExecuteSqlInterpolatedAsync (escrita) — sempre com string interpolada, nunca concatenação manual.
  • DBNull.Value é a forma correta de representar null do C# como parâmetro SQL.

✏️ Praticando

  1. Implemente ListarClientesAsync das três formas (ADO.NET, Dapper, EF Core) num mesmo projeto console e compare a quantidade de código de cada uma.
  2. Implemente a chamada de usp_ClienteInserir (com parâmetro OUTPUT) usando Dapper, como no exemplo da seção 3.