🚀 Introdução ao Dapper: O Micro-ORM de Alta Performance para .NET 10
No ecossistema .NET, a persistência de dados exige um equilíbrio crítico entre produtividade e desempenho bruto. O Dapper surge como uma biblioteca de mapeamento de objetos simples (Micro-ORM) que se destaca por oferecer abstração mínima sobre o ADO.NET, garantindo que desenvolvedores mantenham controle total sobre o SQL executado.
Diferente de ORMs robustos como o Entity Framework, o Dapper utiliza emissão de IL (Intermediate Language) dinâmica para criar mapeadores de objetos em tempo de execução. Isso reduz o overhead de reflexão, agindo como um tradutor ultraeficiente entre registros do banco de dados e objetos C#. No .NET 10, essa eficiência é maximizada pelas melhorias de runtime e novos tipos de coleções span-based.
A Filosofia do Micro-ORM e o Controle de SQL
A premissa central do Dapper é a simplicidade operacional. Ele estende a interface IDbConnection, fornecendo métodos de extensão que permitem consultar e mapear resultados diretamente para objetos POCO (Plain Old CLR Objects). Essa abordagem “SQL-first” elimina as camadas complexas de geração automática de consultas (Query Generation), permitindo que engenheiros otimizem índices e planos de execução (Execution Plans) manualmente.
Em projetos que utilizam o PostgreSQL no .NET 10, essa característica é fundamental para extrair o máximo de performance em operações de leitura e escrita. O desenvolvedor escreve o SQL exato que o banco de dados deve processar, evitando subconsultas ineficientes ou joins desnecessários que ORMs tradicionais poderiam gerar em cenários complexos.
Adequação para Projetos Críticos de Performance
O Dapper é particularmente eficaz em aplicações de alto throughput e sistemas de baixa latência, como APIs de feeds em tempo real ou gateways de pagamento. Ao utilizar consultas SQL brutas, o desenvolvedor evita o “black box” de geradores de consulta, facilitando o tuning de performance diretamente no mecanismo do PostgreSQL através de ferramentas como EXPLAIN ANALYZE.
Para este guia, exploraremos a construção de uma Web API resiliente, aproveitando as capacidades de injeção de dependência (Dependency Injection) nativa e a robustez do driver Npgsql. O foco será na criação de uma camada de acesso a dados (DAL) que suporte carga escalável sem comprometer a clareza do código-fonte ou a segurança das transações.
Requisitos e Ambiente de Desenvolvimento
Para implementar uma solução utilizando Dapper de forma profissional, assume-se um ambiente configurado com as seguintes ferramentas técnicas:
- SDK do .NET 10 instalado e configurado no PATH do sistema.
- Instância ativa do PostgreSQL (Docker é recomendado para isolamento de ambiente).
- Ferramenta de visualização de dados como DBeaver ou Azure Data Studio para validação de esquemas.
- Conhecimento sólido em programação assíncrona (Task-based Asynchronous Pattern – TAP) em C#.
⚖️ Dapper vs. Entity Framework: Desafios e Vantagens Técnicas
O Trade-off da Abstração e o Overhead do EF Core
Enquanto o Entity Framework (EF Core) opera como um ORM completo com uma camada de abstração densa, o Dapper é classificado como um “Object Mapper” puro. A principal distinção reside no processamento: o EF Core precisa traduzir árvores de expressão LINQ em SQL, gerenciar o estado das entidades no Change Tracker e lidar com metadados de mapeamento em memória.
Essa complexidade do EF Core consome ciclos de CPU e memória RAM consideráveis em aplicações de alta escala. Em contraste, o Dapper atua como um wrapper fino sobre o ADO.NET, eliminando o custo de tradução e permitindo que o desenvolvedor entregue consultas SQL otimizadas diretamente ao driver, resultando em menor pressão sobre o Garbage Collector (GC).
Obstáculos na Implementação com Dapper
A escolha pelo Dapper introduz responsabilidades que o EF Core automatiza nativamente. A ausência de suporte ao LINQ-to-SQL obriga a escrita manual de queries, o que aumenta a verbosidade do código e exige maior rigor na manutenção de esquemas. Mudanças no banco de dados não são refletidas automaticamente no código C# sem intervenção manual.
Sem o Change Tracking, operações de atualização (Update) e exclusão (Delete) exigem comandos explícitos para cada campo alterado. Outro ponto crítico é a gestão de migrações; o Dapper não possui ferramentas integradas para versionamento de base de dados, demandando o uso de bibliotecas externas como FluentMigrator, DbUp ou o gerenciamento manual de scripts SQL via ferramentas de CI/CD.
Vantagens Competitivas em Performance e Controle
Onde o Dapper realmente se sobressai é em cenários de leitura intensiva e relatórios complexos. Ao evitar a “caixa preta” do SQL gerado, o engenheiro tem controle granular sobre hints de consulta, índices de cobertura e joins laterais (LATERAL JOIN) específicos do PostgreSQL.
// Exemplo de mapeamento flexível com QueryMultiple para reduzir roundtrips
using var connection = CreateConnection();
using var multi = await connection.QueryMultipleAsync(sql, new { UserId = userId });
var user = await multi.ReadSingleOrDefaultAsync<User>();
var posts = (await multi.ReadAsync<Post>()).ToList();
A capacidade de utilizar métodos como QueryMultiple permite recuperar múltiplos conjuntos de resultados (result sets) em uma única viagem ao servidor (roundtrip). Isso otimiza drasticamente a latência em sistemas distribuídos. Além disso, a proteção contra SQL Injection é nativa através da parametrização automática, garantindo segurança sem sacrificar a flexibilidade do SQL manual.
Mapeamento de Resultados e Flexibilidade
Diferente do EF Core, que impõe convenções rígidas para relacionamentos complexos, o Dapper oferece um mapeamento de resultados altamente maleável. É trivial mapear uma consulta complexa para um Data Transfer Object (DTO) customizado sem configurar o ModelBuilder.
Essa flexibilidade torna o Dapper a ferramenta ideal para microserviços, dashboards analíticos e integração com bases legadas onde as convenções de nomenclatura não seguem padrões modernos. A biblioteca permite lidar com tipos dinâmicos (dynamic) ou mapear para tipos de registro (record) do C#, facilitando a imutabilidade dos dados na camada de aplicação.
🛠️ Configuração do Ambiente e Instalação via CLI
Para implementar uma camada de acesso a dados de alta performance com .NET 10, a configuração do ambiente deve priorizar a agilidade e o controle granular das dependências. Utilizaremos a interface de linha de comando (CLI) para garantir um setup reprodutível e compatível com pipelines de automação.
Provisionamento de Dependências via .NET CLI
O ecossistema Dapper é agnóstico em relação ao provedor de banco de dados, exigindo a instalação do driver ADO.NET específico para o motor escolhido — neste caso, o PostgreSQL (Npgsql). Execute os comandos abaixo no terminal dentro do diretório do projeto para adicionar as bibliotecas necessárias:
# Adiciona o Micro-ORM Dapper ao projeto
dotnet add package Dapper
# Adiciona o driver Npgsql para comunicação com PostgreSQL
dotnet add package Npgsql
Caso precise iniciar a partir de um scaffold estruturado para focar na implementação do repositório, utilize o Git para clonar a estrutura base técnica do tutorial:
git clone https://github.com/grant-dot-dev/dapper_tutorial.git
cd FCC.DapperTutorial
Configuração da String de Conexão no appsettings.json
Diferente de ORMs pesados que gerenciam pools de conexão de forma totalmente abstraída, o Dapper exige que o desenvolvedor forneça uma conexão aberta ou uma string de conexão válida. No arquivo appsettings.json, definimos a chave DefaultConnection seguindo os padrões do PostgreSQL.
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=social_media;Username=postgres;Password=yourpassword"
}
É fundamental que parâmetros como Pooling=true e Maximum Pool Size sejam considerados para aplicações em produção. Esta string é o ponto de entrada para que o driver Npgsql estabeleça o túnel de comunicação binária sobre o qual o Dapper executará as queries SQL otimizadas com baixa latência.
Estrutura de Infraestrutura Inicial
Com as dependências instaladas, o próximo passo técnico consiste em preparar o esquema do banco de dados (Schema Design). Criaremos uma classe utilitária para gerenciar o bootstrap inicial da aplicação, garantindo que as tabelas necessárias existam antes da execução de operações de I/O.
A utilização de comandos CREATE TABLE IF NOT EXISTS é uma prática recomendada para evitar exceções de colisão durante o processo de inicialização. Essa abordagem de “Infrastructure as Code” simples permite que novos desenvolvedores subam o ambiente localmente com um único comando de execução da aplicação.
🏗️ Infraestrutura e Seeding: Preparando o Banco de Dados Social Media
Para garantir a consistência do ambiente de desenvolvimento, a implementação de uma camada de infraestrutura que automatize a criação do schema é fundamental. No ecossistema .NET com Dapper, essa tarefa utiliza estratégias de idempotência para permitir execuções repetitivas sem corromper o estado dos dados.
Estratégias de Idempotência com SQL Raw
Ao lidar com scripts de inicialização, utilizamos duas cláusulas cruciais no PostgreSQL para garantir a estabilidade do sistema:
1. CREATE TABLE IF NOT EXISTS: Garante que a estrutura da tabela seja criada apenas se o objeto não existir, evitando erros de execução em deploys subsequentes no mesmo banco.
2. ON CONFLICT (Column) DO NOTHING: Esta cláusula de “upsert” condicional permite que o comando de inserção ignore registros cujas chaves primárias já existam, prevenindo violações de Unique Constraints.
Implementação da Classe DBUtilities
Abaixo, consolidamos a lógica de criação de tabelas e inserção de dados iniciais (seeding) em uma classe estática dedicada. Observe o uso do Dapper para executar comandos DDL (Data Definition Language) dentro de um escopo de conexão seguro.
using Dapper;
using Npgsql;
using Microsoft.Extensions.Configuration;
namespace DapperTutorial.Infrastructure;
public static class DBUtilities
{
private const string CreateUserSql = @"
CREATE TABLE IF NOT EXISTS ""User"" (
UserId TEXT PRIMARY KEY,
Username TEXT,
FirstName TEXT,
LastName TEXT,
Avatar TEXT,
Email TEXT,
DOB DATE
);";
private const string CreatePostSql = @"
CREATE TABLE IF NOT EXISTS Post (
PostId TEXT PRIMARY KEY,
Likes INTEGER,
Content TEXT,
Timestamp TIMESTAMP,
UserId TEXT,
FOREIGN KEY(UserId) REFERENCES ""User""(UserId)
);";
private const string InsertUsersSql = @"
INSERT INTO ""User"" (UserId, Username, FirstName, LastName, Avatar, Email, DOB)
VALUES
('1', 'iron_man', 'Tony', 'Stark', NULL, 'tony.stark@example.com', '1970-05-29'),
('2', 'batman', 'Bruce', 'Wayne', NULL, 'bruce.wayne@example.com', '1972-11-11'),
('3', 'spiderman', 'Peter', 'Parker', NULL, 'peter.parker@example.com', '1995-08-10')
ON CONFLICT (UserId) DO NOTHING;";
private const string InsertPostsSql = @"
INSERT INTO Post (PostId, Likes, Content, Timestamp, UserId)
VALUES
('p1', 10, 'Hello, world!', '2025-10-12 10:00:00', '1'),
('p2', 5, 'My first post!', '2025-10-12 11:00:00', '2')
ON CONFLICT (PostId) DO NOTHING;";
public static async Task SeedDatabaseAsync(IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection");
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
await using var transaction = await connection.BeginTransactionAsync();
try
{
// Execução de DDL
await connection.ExecuteAsync(CreateUserSql, transaction: transaction);
await connection.ExecuteAsync(CreatePostSql, transaction: transaction);
// Verificação de existência para evitar overhead de re-insert
var userCount = await connection.QuerySingleAsync<int>(
@"SELECT COUNT(*) FROM ""User"";", transaction: transaction);
if (userCount == 0)
{
await connection.ExecuteAsync(InsertUsersSql, transaction: transaction);
await connection.ExecuteAsync(InsertPostsSql, transaction: transaction);
}
await transaction.CommitAsync();
}
catch (Exception ex)
{
await transaction.RollbackAsync();
throw ex;
}
}
}
Integração com o Pipeline da Web API
Para expor essa funcionalidade, utilizamos o modelo de Minimal APIs no Program.cs. Esta abordagem moderna do .NET 10 permite que o desenvolvedor dispare a inicialização do banco de dados através de uma requisição HTTP controlada ou durante o startup do host.
// Registro do endpoint de manutenção
app.MapPost("/seed", async (IConfiguration configuration) =>
{
try
{
await DBUtilities.SeedDatabaseAsync(configuration);
return Results.Ok("Database seeded successfully.");
}
catch (Exception ex)
{
return Results.Problem($"An error occurred while seeding: {ex.Message}");
}
}
Nesta estrutura, o uso de BeginTransactionAsync é vital para a atomicidade. Como estamos manipulando a criação de tabelas e a inserção de registros em sequência, a transação garante que, em caso de falha de rede ou sintaxe, o banco retorne ao estado anterior, evitando “sujeira” no esquema que impediria a execução da aplicação.
📂 Implementando o Repository Pattern com C# Moderno
Para manter a manutenibilidade e a testabilidade da Web API, utilizaremos o Repository Pattern. Este padrão desacopla a lógica de acesso a dados da lógica de negócio, permitindo que o restante da aplicação interaja com coleções de objetos sem depender diretamente de drivers de banco de dados específicos.
Definição dos Modelos de Dados (Entities)
Utilizamos modificadores sealed em nossos modelos para otimização de performance. Em aplicações de alta carga, marcar classes como seladas impede a herança e permite que o compilador JIT (Just-In-Time) realize otimizações como a desvirtualização de chamadas. Abaixo, definimos as entidades core:
// User.cs
public sealed class User
{
public string UserId { get; init; } = string.Empty;
public string Username { get; init; } = string.Empty;
public string FirstName { get; init; } = string.Empty;
public string LastName { get; init; } = string.Empty;
public string? Avatar { get; init; }
public string Email { get; init; } = string.Empty;
public DateOnly? DOB { get; init; }
}
// Post.cs
public sealed class Post
{
public string PostId { get; init; } = string.Empty;
public int Likes { get; init; } = 0;
public string Content { get; init; } = string.Empty;
public DateTime Timestamp { get; init; } = DateTime.MinValue;
public string UserId { get; init; } = string.Empty;
}
Contrato de Abstração: IRepository
A interface IRepository define o contrato de comunicação assíncrona. O uso sistemático de Task<T> garante que todas as operações de I/O de banco de dados não bloqueiem as threads do servidor Kestrel, aumentando a escalabilidade horizontal da API.
using DapperTutorial.Models;
namespace DapperTutorial.Application;
public interface IRepository
{
Task<IEnumerable<User>> GetUsersAsync();
Task<User?> GetUserByIdAsync(string userId);
Task<IEnumerable<Post>> GetPostsAsync();
Task<Post?> GetPostByIdAsync(string postId);
Task<IEnumerable<Post>> GetPostsByUserAsync(string userId);
Task<bool> CreateUserAsync(User user);
Task<bool> UpdateUserAsync(User user);
Task<bool> DeleteUserAsync(string userId);
}
Implementação Concreta e Injeção de Dependência
A classe Repository implementa a lógica utilizando o driver Npgsql e Dapper. Note o uso de Primary Constructors (recurso do C# 12/13) para injetar IConfiguration de forma concisa, eliminando o boilerplate de declaração de campos privados.
using Dapper;
using DapperTutorial.Application;
using DapperTutorial.Models;
using Npgsql;
namespace DapperTutorial.Infrastructure;
public class Repository : IRepository
{
private readonly string _connectionString;
public Repository(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Missing connection string: DefaultConnection");
}
private NpgsqlConnection CreateConnection() => new(_connectionString);
public async Task> GetUsersAsync()
{
const string sql = @"
SELECT UserId, Username, FirstName, LastName, Avatar, Email, DOB
FROM ""User""
ORDER BY Username;";
await using var connection = CreateConnection();
var users = await connection.QueryAsync(sql);
return users.ToList();
}
public async Task GetUserByIdAsync(string userId)
{
const string sql = @"
SELECT UserId, Username, FirstName, LastName, Avatar, Email, DOB
FROM ""User""
WHERE UserId = @UserId;";
await using var connection = CreateConnection();
return await connection.QuerySingleOrDefaultAsync(sql, new { UserId = userId });
}
// Métodos adicionais de Posts seguem o mesmo padrão de conexão...
}
Segurança na Inicialização e Fail-Fast
Ao recuperar a string de conexão, o padrão ?? throw implementa o princípio de fail-fast. Se a configuração estiver ausente, a aplicação lança uma exceção descritiva imediatamente. Isso evita que erros silenciosos de conexão ocorram apenas quando o primeiro usuário tentar acessar o sistema, facilitando a depuração em ambientes de staging.
🔍 Consultas Avançadas: QueryAsync e Seleção Única
A flexibilidade do Dapper reside na sua capacidade de mapear resultados SQL diretamente para objetos C# com overhead quase nulo. Dominar os diferentes métodos de consulta é essencial para lidar com cardinalidade e garantir a integridade dos dados retornados para a camada de aplicação.
Mapeamento de Coleções com QueryAsync
O método QueryAsync<T> é o motor principal para recuperação de listas. Ele mapeia cada linha do conjunto de resultados para uma nova instância da entidade definida. Tecnicamente, o Dapper retorna um IEnumerable<T> que, por padrão, não é nulo; se a query não encontrar registros, você receberá uma coleção vazia, eliminando a necessidade de verificações de nulidade antes de loops.
public async Task<IEnumerable<User>> GetUsersAsync()
{
const string sql = @"
SELECT UserId, Username, FirstName, LastName, Avatar, Email, DOB
FROM ""User""
ORDER BY Username;";
await using var connection = CreateConnection();
var users = await connection.QueryAsync<User>(sql);
return users.ToList();
}
Garantindo a Integridade com QuerySingleOrDefaultAsync
Para operações de busca por ID ou chaves únicas, o QuerySingleOrDefaultAsync é a escolha técnica recomendada. Este método valida que a consulta retorna no máximo um registro. Caso o banco retorne múltiplos registros (o que indicaria uma falha de unicidade no banco), o Dapper lança uma InvalidOperationException imediatamente.
Este comportamento atua como uma proteção de integridade lógica. Se uma busca por CPF ou E-mail retornar mais de uma linha, o sistema interrompe o processamento antes de expor dados inconsistentes, permitindo que a equipe de engenharia identifique falhas em constraints de banco de dados.
public async Task<User?> GetUserByIdAsync(string userId)
{
const string sql = @"
SELECT UserId, Username, FirstName, LastName, Avatar, Email, DOB
FROM ""User""
WHERE UserId = @UserId;";
await using var connection = CreateConnection();
return await connection.QuerySingleOrDefaultAsync<User>(sql, new { UserId = userId });
}
Seleção de Topo com QueryFirstOrDefaultAsync
Diferente do Single, o método QueryFirstOrDefaultAsync é projetado para cenários onde apenas o primeiro registro importa, mesmo que existam outros. Ele não lança exceções para multiplicidade; ele captura o primeiro item e descarta os demais, sendo ideal para consultas ordenadas por data (ex: “último login”).
Em ambos os métodos, se nenhum registro for encontrado, o retorno será null para tipos de referência. A escolha entre eles define a semântica de erro do sistema: use Single quando a duplicidade for um erro crítico e First quando a multiplicidade for permitida mas apenas um item for necessário para o contexto atual.
✍️ Manipulação de Dados: Insert e Update com ExecuteAsync
Para operações que alteram o estado do banco de dados, como comandos DML (Insert, Update, Delete), o Dapper fornece o método ExecuteAsync. Este método é otimizado para retornar um inteiro representando o número de linhas afetadas, permitindo validações rápidas de persistência.
Persistência de Novos Registros com Mapeamento Direto
Uma das funcionalidades mais eficientes do Dapper é o binding de parâmetros diretamente de objetos complexos. Ao passar uma instância de classe para o ExecuteAsync, o motor utiliza cache de reflexão para mapear propriedades do C# aos parâmetros SQL identificados por @.
public async Task<bool> CreateUserAsync(User user)
{
const string sql = @"
INSERT INTO ""User"" (
UserId, Username, FirstName, LastName, Avatar, Email, DOB
) VALUES (
@UserId, @Username, @FirstName, @LastName, @Avatar, @Email, @DOB
);";
await using var connection = CreateConnection();
var rowsAffected = await connection.ExecuteAsync(sql, user);
return rowsAffected > 0;
}
Tratamento de Tipos Anuláveis e Nulidade
No modelo de dados de redes sociais, campos como Avatar ou Data de Nascimento são frequentemente opcionais. O Dapper trata tipos Nullable<T> de forma transparente, injetando DBNull.Value automaticamente quando a propriedade for nula. Isso mantém a integridade do banco de dados sem a necessidade de código condicional repetitivo.
Atualização de Entidades Existentes
O padrão para atualizações utiliza a mesma lógica de mapeamento. Ao executar um UPDATE, passamos o objeto completo; o Dapper localiza a propriedade identificadora (ex: UserId) para a cláusula WHERE e utiliza os demais campos para atualizar as colunas correspondentes no PostgreSQL.
public async Task<bool> UpdateUserAsync(User user)
{
const string sql = @"
UPDATE ""User""
SET Username = @Username,
FirstName = @FirstName,
LastName = @LastName,
Avatar = @Avatar,
Email = @Email,
DOB = @DOB
WHERE UserId = @UserId;";
await using var connection = CreateConnection();
var affectedRows = await connection.ExecuteAsync(sql, user);
return affectedRows > 0;
}
Esta abordagem reduz o boilerplate, pois não exige a criação de objetos anônimos manuais para cada comando. A clareza do SQL escrito pelo desenvolvedor, combinada com a automação de parâmetros do Dapper, resulta em uma camada de persistência robusta e de fácil manutenção evolutiva.
🛡️ Segurança Crítica: Parametrização vs. SQL Injection
A segurança da camada de dados é um requisito não funcional obrigatório. O Dapper, embora ofereça liberdade para escrever SQL puro, implementa mecanismos de parametrização rigorosos para mitigar vulnerabilidades de Injeção de SQL (SQL Injection), que podem comprometer toda a base de dados.
O Risco da Interpolação de Strings
O erro mais comum ao utilizar Micro-ORMs é a construção de queries dinâmicas via interpolação de strings ($""). Ao inserir variáveis diretamente na string SQL, você permite que o input do usuário altere a estrutura lógica da consulta, o que é catastrófico.
Considere o padrão inseguro abaixo que não deve ser utilizado:
// Exemplo de código vulnerável - NÃO UTILIZE
var sql = $"SELECT * FROM \"User\" WHERE Username = '{username}'";
Não há erros de sintaxe no bloco de código fornecido. No entanto, o exemplo apresenta uma vulnerabilidade de segurança conhecida como injeção de SQL. Para corrigir isso, é recomendável utilizar parâmetros ou preparar as consultas SQL para evitar a injeção de código malicioso.
Aqui está um exemplo de como o código poderia ser corrigido utilizando parâmetros (considerando um contexto de aplicação .NET com ADO.NET):
“`csharp
using System.Data.SqlClient;
// …
string connectionString = “SuaConnectionString”;
string username = “NomeDoUsuario”;
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
string query = “SELECT * FROM \”User\” WHERE Username = @username”;
using (SqlCommand command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue(“@username”, username);
// Execute a consulta
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
// Processar resultados
}
}
}
}
“`
Lembre-se de que o exemplo original em si não contém erros de sintaxe, mas apresenta uma vulnerabilidade de segurança. A solução proposta visa mitigar essa vulnerabilidade.
Se um atacante enviar o valor ' OR '1'='1, a query resultante será processada pelo PostgreSQL como um comando para retornar todos os registros da tabela, ignorando o filtro de segurança e expondo dados de terceiros.
Mecanismo de Defesa: Parametrização Binária
O Dapper resolve esse risco através da parametrização obrigatória com placeholders @. Quando utilizamos essa técnica, a estrutura da query é enviada ao banco de dados separadamente dos dados, garantindo que o motor SQL trate os valores estritamente como literais.
const string sql = @"SELECT * FROM ""User"" WHERE Username = @Username";
// O Dapper gerencia a sanitização e o mapeamento de tipos
var user = await connection.QuerySingleOrDefaultAsync<User>(sql, new { Username = username });
Nesse cenário, mesmo que o valor de @Username contenha comandos maliciosos, o PostgreSQL não os executará, pois o plano de execução já foi pré-compilado para tratar aquele espaço apenas como um dado de texto. Essa separação entre instrução e dado é o padrão ouro de segurança em sistemas .NET modernos.
🗑️ Exclusão e Processamento em Lote (Batch Processing)
A exclusão de registros e o processamento em lote exigem estratégias de execução eficientes para evitar gargalos de rede (Network Latency). No Dapper, a remoção de um registro individual é direta, mas o tratamento de coleções de IDs exige abordagens otimizadas para performance.
Remoção de Registros Individuais
Para deletar um item, mapeamos o identificador único para o parâmetro SQL. Esta operação é atômica e utiliza a infraestrutura de parâmetros para garantir segurança enquanto executa o comando DELETE de forma performática.
public async Task<bool> DeleteUserAsync(string userId)
{
const string sql = @"DELETE FROM ""User"" WHERE UserId = @UserId;";
await using var connection = CreateConnection();
var affectedRows = await connection.ExecuteAsync(sql, new { UserId = userId });
return affectedRows > 0;
}
Otimização com ANY(@UserIds) no PostgreSQL
Para realizar operações em lote (Batch) em uma única viagem ao servidor, delegamos a lógica ao motor do PostgreSQL utilizando o operador ANY. Isso é muito mais eficiente do que realizar múltiplos comandos DELETE dentro de um loop no C#.
public async Task<bool> DeleteUsersAsync(IEnumerable<string> userIds)
{
const string sql = @"DELETE FROM ""User"" WHERE UserId = ANY(@UserIds)";
await using var connection = CreateConnection();
var affectedRows = await connection.ExecuteAsync(sql, new { UserIds = userIds.ToArray() });
return affectedRows > 0;
}
Ao converter a lista para um array (ToArray()), o driver Npgsql mapeia a coleção diretamente para o tipo de array nativo do banco. Isso reduz a carga no servidor SQL e o tempo total de resposta da API, especialmente em operações que envolvem centenas ou milhares de registros simultâneos.
⛓️ Garantindo Atomicidade com Transações no Dapper
A integridade dos dados é sustentada pela atomicidade das operações. Quando múltiplas escritas dependem uma da outra, utilizamos transações para garantir que todas as alterações sejam confirmadas (Commit) ou revertidas (Rollback) em caso de qualquer falha técnica.
Implementação Prática com BeginTransactionAsync
Para utilizar transações com Dapper, abrimos a conexão manualmente e iniciamos um objeto IDbTransaction. Cada comando executado dentro desse escopo deve receber explicitamente o objeto da transação no parâmetro transaction dos métodos de extensão do Dapper.
public async Task<bool> CreateUserWithWelcomePostAsync(User user, Post welcomePost)
{
const string insertUserSql = @"
INSERT INTO ""User"" (UserId, Username, FirstName, LastName, Email, DOB)
VALUES (@UserId, @Username, @FirstName, @LastName, @Email, @DOB);";
const string insertPostSql = @"
INSERT INTO Post (PostId, Likes, Content, Timestamp, UserId)
VALUES (@PostId, @Likes, @Content, @Timestamp, @UserId);";
await using var connection = CreateConnection();
await connection.OpenAsync();
// Iniciando a transação assíncrona
await using var transaction = await connection.BeginTransactionAsync();
try
{
// Executando as operações vinculadas à mesma transação
await connection.ExecuteAsync(insertUserSql, user, transaction: transaction);
await connection.ExecuteAsync(insertPostSql, welcomePost, transaction: transaction);
// Se ambas as operações forem bem-sucedidas, confirmamos as alterações
await transaction.CommitAsync();
return true;
}
catch (Exception)
{
// Em caso de qualquer erro, a transação é revertida integralmente
await transaction.RollbackAsync();
throw;
}
}
É fundamental o uso de await using para o objeto da transação e da conexão, garantindo a liberação correta de recursos do pool. Passar o parâmetro transaction: transaction é obrigatório; se omitido, o Dapper tentará executar o comando fora da transação ativa, o que resultará em erros de concorrência no driver Npgsql.
Transações garantem que, se a inserção de um usuário for bem-sucedida mas o log de boas-vindas falhar, o usuário não será criado “pela metade”. Essa consistência é o que diferencia sistemas amadores de aplicações enterprise resilientes e confiáveis.
Fonte: freecodecamp.org.
Curadoria e Insights: Redação YTI&W (Developers).