1. Por que aprender isso, se existe EF Core e Dapper
O capítulo 13 já mostrou EF Core e o Módulo 2 (capítulo 13) mostrou Dapper — as duas formas mais
usadas no dia a dia. Mas vale entender a camada por baixo delas, System.Data.SqlClient
(ou seu sucessor, Microsoft.Data.SqlClient), por três motivos práticos:
controle total sobre performance em código crítico, projetos que não podem (ou não devem) trazer
dependências extras, e — o mais importante para você agora — entender exatamente o que um ORM
está fazendo por você é o que separa "usar uma ferramenta" de "saber o que ela esconde".
2. SqlConnection — a conexão com o banco
string connectionString = "Server=localhost;Database=CursoDb;Trusted_Connection=True;TrustServerCertificate=True;";
using var conexao = new SqlConnection(connectionString);
await conexao.OpenAsync();
// ... usar a conexão ...
// o 'using' garante Dispose() no final do escopo, fechando a conexão automaticamente
Fechar uma SqlConnection não derruba a conexão TCP de verdade com o
SQL Server — o ADO.NET mantém um pool de conexões físicas já abertas em segundo
plano. "Abrir" uma conexão geralmente só pega uma emprestada do pool; "fechar" a devolve para
reuso. É por isso que abrir/fechar uma SqlConnection por operação
(como todos os exemplos deste curso já fazem) é seguro e barato — o custo caro (handshake TCP,
autenticação) só acontece quando o pool precisa criar uma conexão física nova.
3. SqlCommand e parâmetros — nunca concatene valores na query
using var comando = new SqlCommand(
"SELECT Id, Nome, Email FROM dbo.Cliente WHERE Nome LIKE @Nome",
conexao);
comando.Parameters.Add("@Nome", SqlDbType.NVarChar, 100).Value = "%" + termoBusca + "%";
comando.Parameters.Add(...) é a versão C# de
sp_executesql com parâmetros tipados — nunca construa a query como
"...WHERE Nome LIKE '%" + termoBusca + "%'". Isso é SQL injection
exatamente como visto naquele capítulo, só que a porta de entrada agora é o código C#, não o
T-SQL dinâmico.
4. Os três jeitos de executar um comando
| Método | Quando usar | Devolve |
|---|---|---|
ExecuteNonQueryAsync() | INSERT/UPDATE/DELETE, sem result set | Número de linhas afetadas |
ExecuteScalarAsync() | Quando a query devolve um único valor | O primeiro valor da primeira linha, como object |
ExecuteReaderAsync() | Quando a query devolve várias linhas/colunas | Um SqlDataReader para iterar |
// ExecuteNonQuery — um UPDATE
using (var cmd = new SqlCommand("UPDATE dbo.Cliente SET Ativo = 0 WHERE Id = @Id", conexao))
{
cmd.Parameters.AddWithValue("@Id", 42);
int linhasAfetadas = await cmd.ExecuteNonQueryAsync();
}
// ExecuteScalar — contar registros
using (var cmd = new SqlCommand("SELECT COUNT(*) FROM dbo.Cliente WHERE Ativo = 1", conexao))
{
int total = (int)await cmd.ExecuteScalarAsync()!;
}
// ExecuteReader — várias linhas
using (var cmd = new SqlCommand("SELECT Id, Nome FROM dbo.Cliente WHERE Ativo = 1", conexao))
using (var leitor = await cmd.ExecuteReaderAsync())
{
while (await leitor.ReadAsync())
{
int id = leitor.GetInt32(leitor.GetOrdinal("Id"));
string nome = leitor.GetString(leitor.GetOrdinal("Nome"));
}
}
5. Mapeamento manual — o que Dapper faz por você
Sem um micro-ORM, mapear SqlDataReader para objetos é responsabilidade
sua. Um método auxiliar reaproveitável evita repetir esse código toda vez:
static Cliente MapearCliente(SqlDataReader leitor) => new()
{
Id = leitor.GetInt32(leitor.GetOrdinal("Id")),
Nome = leitor.GetString(leitor.GetOrdinal("Nome")),
Email = leitor.GetString(leitor.GetOrdinal("Email")),
Ativo = leitor.GetBoolean(leitor.GetOrdinal("Ativo")),
};
var clientes = new List<Cliente>();
using var leitor = await comando.ExecuteReaderAsync();
while (await leitor.ReadAsync())
clientes.Add(MapearCliente(leitor));
6. Transações com SqlTransaction
Assim como BEGIN TRANSACTION/COMMIT no
T-SQL (Módulo 2, capítulo 5), o ADO.NET tem seu próprio controle de transação — útil quando você
precisa coordenar múltiplos comandos separados a partir do C#, não dentro de uma
única procedure:
await using var conexao = new SqlConnection(connectionString);
await conexao.OpenAsync();
await using var transacao = await conexao.BeginTransactionAsync();
try
{
await using (var cmd1 = new SqlCommand("UPDATE dbo.Conta SET Saldo = Saldo - @V WHERE Id = @Origem", conexao, (SqlTransaction)transacao))
{
cmd1.Parameters.AddWithValue("@V", 100m);
cmd1.Parameters.AddWithValue("@Origem", 1);
await cmd1.ExecuteNonQueryAsync();
}
await using (var cmd2 = new SqlCommand("UPDATE dbo.Conta SET Saldo = Saldo + @V WHERE Id = @Destino", conexao, (SqlTransaction)transacao))
{
cmd2.Parameters.AddWithValue("@V", 100m);
cmd2.Parameters.AddWithValue("@Destino", 2);
await cmd2.ExecuteNonQueryAsync();
}
await transacao.CommitAsync();
}
catch
{
await transacao.RollbackAsync();
throw;
}
Sempre que possível, prefira colocar essa lógica dentro de uma única procedure (como
usp_ContaTransferir, Módulo 2 capítulo 5) — a transação inteira roda
no servidor, com menos idas e voltas de rede. Use SqlTransaction do
lado do C# quando os comandos realmente precisam ser decididos em tempo de execução pela
aplicação (por exemplo, coordenando chamadas a mais de um banco de dados diferente).
7. Um Repository só com ADO.NET — sem Dapper, sem EF Core
public class ClienteRepositoryAdoNet : IClienteRepository
{
private readonly string _connectionString;
public ClienteRepositoryAdoNet(string connectionString) => _connectionString = connectionString;
public async Task<Cliente?> ObterPorIdAsync(int id)
{
await using var conexao = new SqlConnection(_connectionString);
await using var comando = new SqlCommand("dbo.usp_ClienteObter", conexao)
{
CommandType = CommandType.StoredProcedure
};
comando.Parameters.Add("@Id", SqlDbType.Int).Value = id;
await conexao.OpenAsync();
await using var leitor = await comando.ExecuteReaderAsync();
return await leitor.ReadAsync() ? MapearCliente(leitor) : null;
}
private static Cliente MapearCliente(SqlDataReader leitor) => new()
{
Id = leitor.GetInt32(leitor.GetOrdinal("Id")),
Nome = leitor.GetString(leitor.GetOrdinal("Nome")),
Email = leitor.GetString(leitor.GetOrdinal("Email")),
};
}
Essa classe implementa exatamente a mesma interface IClienteRepository
do capítulo 12 — o resto da aplicação (Service, Controller) não muda uma linha, não importa qual
das três abordagens (ADO.NET, Dapper, EF Core) esteja por trás da interface. Essa é a recompensa
prática de programar contra abstrações.
8. Onde isso te leva
Com as três camadas de acesso a dados entendidas — ADO.NET puro, Dapper e EF Core — você tem o quadro completo de opções para qualquer projeto C#/SQL Server, e o entendimento de baixo nível para escolher com intenção, não por hábito. Isso fecha o Módulo 1; os módulos seguintes constroem arquiteturas inteiras (MVC, APIs, Worker Services, Desktop) em cima dessa mesma base.