Criando um CRUD com Dapper

Criar um CRUD com Dapper é uma das formas mais diretas de acessar um banco de dados no .NET sem abrir mão do controle sobre o SQL. Neste artigo, vamos montar as quatro operações básicas de forma simples e objetiva, usando .NET 7 e SQL Server.

Ao final, você terá um projeto console que insere, lê, altera e exclui registros. Além disso, vai entender por que os parâmetros são obrigatórios e como tipar os resultados.

O que é o Dapper

Dapper é uma biblioteca que facilita a nossa vida na hora de consultar, editar, excluir e inserir dados em um banco. Ele funciona com SQL Server, PostgreSQL, MySQL ou qualquer outro banco que tenha um provider ADO.NET.

Segundo o repositório oficial, o Dapper é um mapeador objeto relacional leve, também chamado de micro ORM. Ele foi criado pelo time do Stack Overflow para uso em produção e depois liberado como software livre.

Na prática, o Dapper complementa o ADO.NET, que é o alicerce de acesso a dados da Microsoft. No nosso caso, o provider usado é o pacote Microsoft.Data.SqlClient, responsável por conversar com o SQL Server.

Dessa forma, o Dapper apenas incrementa a conexão, adicionando métodos de extensão como Execute e Query ao SqlConnection. Parece um bicho de sete cabeças, mas é bem simples, como você vai ver a seguir.

Por que usar um CRUD com Dapper

O principal motivo é o equilíbrio entre controle e produtividade. Você escreve o SQL que quiser, enquanto o Dapper cuida da parte repetitiva de criar comandos, passar parâmetros e mapear colunas para objetos.

Além disso, o desempenho é um ponto forte. Os benchmarks publicados no repositório mostram o Dapper à frente da maioria dos ORMs completos e bem perto do ADO.NET escrito à mão.

Por outro lado, você perde recursos como rastreamento de mudanças e migrações automáticas. Se quiser comparar os dois mundos, o artigo sobre Dapper e Entity Framework Core aprofunda essa escolha.

O que é CRUD

CRUD é a sigla para CREATE (criar), READ (ler), UPDATE (alterar) e DELETE (excluir). Ou seja, são as quatro operações básicas que temos nos bancos de dados.

O CRUD é conhecido como a base que todo programador ou programadora precisa saber. Inclusive, é quase certeza que, em alguma entrevista de emprego, você será desafiado a implementá-lo.

No SQL, cada letra corresponde a uma instrução. Assim, temos INSERT para criar, SELECT para ler, UPDATE para alterar e DELETE para excluir.

Portanto, montar um CRUD com Dapper significa basicamente escrever essas quatro instruções e executá-las com os métodos certos da biblioteca.

Ferramentas necessárias

Para seguir com este tutorial, você precisa do .NET e do SQL Server instalados. Os artigos abaixo detalham a instalação dessas ferramentas:

Você também vai precisar de um editor de SQL para criar o banco. O original deste tutorial usava o Azure Data Studio, mas a Microsoft aposentou essa ferramenta em 28 de fevereiro de 2026.

Por isso, hoje a recomendação oficial é usar o Visual Studio Code com a extensão MSSQL ou o SQL Server Management Studio. Qualquer um deles executa os scripts deste artigo sem mudanças.

Criando o projeto do CRUD com Dapper

Vamos começar criando um projeto novo com o comando dotnet new, conforme mostrado abaixo. Nesse caso, estou usando o .NET 7.

dotnet new console -o DapperCrud
cd DapperCrud

O template console do .NET 7 já usa instruções de nível superior. Então, todo o código deste artigo pode ficar direto no arquivo Program.cs, sem classe nem método Main.

Preparando o banco de dados

Antes de fazer qualquer operação do CRUD com Dapper, precisamos de um banco criado e de pelo menos uma tabela. Não vou estender o artigo para esse lado, então abaixo está o código para criar tudo o que precisamos.

CREATE DATABASE [DapperCrud]
GO

USE [DapperCrud]
GO

CREATE TABLE [Category] (
    [Id] INT NOT NULL PRIMARY KEY IDENTITY (1,1),
    [Title] NVARCHAR(80) NOT NULL,
    [Slug] VARCHAR(80) NOT NULL,
    [Description] TEXT
)

Basta executar o script acima conectado ao seu SQL Server, seja ele no Docker ou no SQL Server Express. A coluna Id usa IDENTITY, então o próprio banco gera o valor a cada inserção.

Usaremos essa única tabela Category em todas as etapas do CRUD com Dapper. Assim, fica fácil conferir no banco o efeito de cada comando.

Adicionando o Dapper ao projeto

Nosso próximo passo é adicionar os dois pacotes que precisamos para realizar operações no banco. Execute os comandos abaixo dentro da pasta do projeto.

dotnet add package Microsoft.Data.SqlClient
dotnet add package Dapper

O primeiro pacote é o provider do SQL Server, que abre a conexão e executa os comandos. Já o segundo traz os métodos de extensão que deixam o CRUD com Dapper tão enxuto.

No topo do Program.cs, importe os dois namespaces com using Dapper; e using Microsoft.Data.SqlClient;. Sem eles, os métodos de extensão não aparecem no objeto de conexão.

Abrindo a conexão do CRUD com Dapper

Agora vamos ao que interessa, que é realizar as operações no banco. Para isso, precisamos primeiro abrir uma conexão, o que fazemos com o SqlConnection.

using Dapper;
using Microsoft.Data.SqlClient;

const string connectionString =
    "Server=localhost,1433;Database=DapperCrud;User ID=sa;Password=your_password;Trusted_Connection=False;TrustServerCertificate=True;";

await using (var connection = new SqlConnection(connectionString))
{
    // ... Operações
}

Note que já aproveitamos para definir a connection string como uma constante. Lembre-se de que ela não deve ficar exposta assim no código, e sim configurada via Application Settings, como vemos no curso Fundamentos do Azure, Git, GitHub e DevOps.

Se quiser entender cada parâmetro dessa string, o artigo descomplicando as connection strings do SQL Server explica um por um.

Cuidados com TrustServerCertificate

A partir da versão 4.0 do Microsoft.Data.SqlClient, a criptografia vem habilitada por padrão. Por isso, sem um certificado confiável no servidor local, a conexão falha, e o TrustServerCertificate=True contorna esse problema.

No entanto, a documentação da Microsoft alerta que essa opção pula a validação da identidade do servidor. Portanto, use esse ajuste apenas em ambientes de desenvolvimento controlados, nunca em produção.

O papel do await using

Outro ponto é o uso do await antes do using. Esse uso é opcional, mas aqui vou mostrar os métodos sendo executados de forma assíncrona.

É apenas uma demonstração, e o async/await não muda nada no resultado deste cenário. Em uma API, porém, as chamadas assíncronas liberam threads enquanto o banco responde.

Também não precisamos chamar Open manualmente. Afinal, o Dapper abre a conexão quando ela está fechada e a fecha novamente ao terminar o comando.

Create no CRUD com Dapper

Como o Dapper estende a conexão, temos o método ExecuteAsync no objeto connection que abrimos no passo anterior. Esse método é bem simples e espera apenas dois parâmetros.

// Formato geral: instrução SQL e objeto com os parâmetros
await connection.ExecuteAsync("SQL STATEMENT", parameters);

Como o Id já é autoincremento na tabela Category, informamos apenas os campos restantes. Assim, temos a seguinte instrução SQL.

INSERT INTO
    [Category] ([Title], [Slug], [Description])
VALUES(
    @title,
    @slug,
    @description)

IMPORTANTE: nunca utilize concatenação de strings nas instruções SQL, porque isso pode resultar em SQL Injection. Utilize sempre parâmetros, como os demonstrados acima com @, que não precisam de aspas duplas nem simples.

A documentação do Microsoft.Data.SqlClient reforça essa regra. Segundo ela, a injeção acontece quando a aplicação trata uma entrada não confiável como T-SQL executável, e a defesa é usar parâmetros para os valores.

Usando tipos anônimos como parâmetros

Embora o C# seja uma linguagem fortemente tipada, podemos usar tipos anônimos para ganhar tempo. Também existe a opção de usar records, mas vou focar aqui nos tipos anônimos pela praticidade.

await using (var connection = new SqlConnection(connectionString))
{
    await connection.ExecuteAsync(
        "INSERT INTO [Category] ([Title], [Slug], [Description]) VALUES(@title, @slug, @description)",
        new
        {
            title = "Backend",
            slug = "backend",
            description = "Learn everything about backend in this complete career."
        });
}

O Dapper lê as propriedades do objeto anônimo e cria um parâmetro para cada uma. Por isso, o nome da propriedade precisa bater com o nome usado depois do @ na instrução.

Pronto, temos a primeira letra do CRUD com Dapper: um trecho de código que insere uma linha no SQL Server de forma simples e eficiente. Ao rodar a aplicação, o resultado na tabela foi este:

1	Backend	backend	Learn everything about backend in this complete career.

Retornando o Id com ExecuteScalar

Apenas por curiosidade, existe uma sintaxe no SQL Server que retorna o Id do registro que acabou de ser inserido. Basta usar o SCOPE_IDENTITY, conforme mostrado abaixo.

SELECT CAST(SCOPE_IDENTITY() AS INT)

Segundo a documentação do SQL Server, o SCOPE_IDENTITY devolve o último valor de identidade gerado no mesmo escopo. Diferente do @@IDENTITY, ele não sofre interferência de triggers que inserem em outras tabelas.

Podemos combinar o SCOPE_IDENTITY com o ExecuteScalarAsync<int> do Dapper e retornar o Id da linha que acabamos de inserir.

await using (var connection = new SqlConnection(connectionString))
{
    var id = await connection.ExecuteScalarAsync<int>(
        "INSERT INTO [Category] ([Title], [Slug], [Description]) VALUES(@title, @slug, @description);SELECT CAST(SCOPE_IDENTITY() AS INT)",
        new
        {
            title = "Backend",
            slug = "backend",
            description = "Learn everything about backend in this complete career."
        });

    Console.WriteLine(id);
}

O ExecuteScalarAsync<T> devolve o valor da primeira coluna da primeira linha do resultado. Nesse caso, essa coluna é justamente o Id convertido para int.

Esse padrão é muito útil no CRUD com Dapper quando a aplicação precisa devolver o recurso criado. Por exemplo, uma API pode responder com o Id novo logo após o cadastro.

Read no CRUD com Dapper

A leitura é a parte do CRUD com Dapper que mais oferece opções.

Existem várias formas de ler registros do banco. Entre elas estão o QueryAsync, o QueryAsync<T>, o QueryFirstOrDefaultAsync e o QueryFirstOrDefaultAsync<T>, conforme mostrado abaixo.

// Lista todas as categorias
var categories = await connection.QueryAsync(
    "SELECT [Id], [Title], [Slug], [Description] FROM [Category]");
foreach (var item in categories)
    Console.WriteLine(item.Title);

// Busca a categoria 1
var category = await connection.QueryFirstOrDefaultAsync(
    "SELECT [Id], [Title], [Slug], [Description] FROM [Category] WHERE [Id]=@id",
    new { id = 1 });
Console.WriteLine(category?.Title);

Sem um tipo genérico, o Dapper devolve objetos dynamic. Isso é prático para testes rápidos, mas você perde a verificação do compilador e o IntelliSense.

Tipando o resultado com records

Caso queira tipar explicitamente o retorno, podemos usar uma struct, uma class ou um record. Neste caso, vou usar um record pela simplicidade.

public record Category(int Id, string Title, string Slug, string? Description);

Como a coluna Description aceita nulos, o último parâmetro usa string?. Além disso, os nomes e a ordem dos parâmetros acompanham as colunas do SELECT, o que facilita o mapeamento pelo construtor.

// Lista todas as categorias
var categories = await connection.QueryAsync<Category>(
    "SELECT [Id], [Title], [Slug], [Description] FROM [Category]");
foreach (var item in categories)
    Console.WriteLine(item.Title);

// Busca a categoria 1
var category = await connection.QueryFirstOrDefaultAsync<Category>(
    "SELECT [Id], [Title], [Slug], [Description] FROM [Category] WHERE [Id]=@id",
    new { id = 1 });
Console.WriteLine(category?.Title);

Repare que o QueryFirstOrDefaultAsync devolve null quando nenhuma linha é encontrada. Por isso, o operador ?. evita uma exceção ao imprimir o título.

Boas práticas de leitura

Algumas regras simples deixam a leitura do CRUD com Dapper mais segura.

Sempre liste as colunas no SELECT em vez de usar SELECT *. Dessa forma, você traz apenas o que precisa e evita surpresas quando alguém adicionar colunas à tabela.

Também prefira o método mais específico para cada caso. Por exemplo, o QuerySingleAsync lança exceção se houver zero ou mais de uma linha, o que ajuda a detectar dados inconsistentes.

Update no CRUD com Dapper

Os métodos a seguir são bem simples e seguem a mesma linha do CREATE. Ou seja, usamos o ExecuteAsync para executar a instrução SQL.

// Altera o título da categoria 1
await connection.ExecuteAsync(
    "UPDATE [Category] SET [Title]=@title WHERE [Id]=@id",
    new { id = 1, title = "Frontend" });

O ExecuteAsync devolve o número de linhas afetadas. Com isso, você consegue verificar se o registro realmente existia antes de informar sucesso ao usuário.

Lembre-se de sempre incluir o WHERE em um update do CRUD com Dapper. Caso contrário, a instrução altera todas as linhas da tabela de uma só vez.

Delete no CRUD com Dapper

Por fim, o DELETE é ainda mais simples, porque basta informar o Id do registro a ser excluído.

// Exclui a categoria 1
await connection.ExecuteAsync(
    "DELETE FROM [Category] WHERE [Id]=@id",
    new { id = 1 });

Assim como no update, o WHERE é essencial aqui. Da mesma forma, o retorno do método indica quantas linhas foram removidas.

Com isso, o CRUD com Dapper está completo. Em poucas linhas, cobrimos inserção, leitura, alteração e exclusão, sempre com parâmetros.

Próximos passos depois do CRUD com Dapper

Em um projeto real, o código de acesso a dados raramente fica solto no Program.cs. Normalmente, ele vai para classes de repositório, que concentram as consultas de cada entidade.

Quando várias operações precisam ser gravadas juntas, também entram em cena as transações. Nesse cenário, o artigo sobre Dapper com Unit of Work e Repository Pattern mostra como organizar tudo isso.

Por fim, vale conhecer a documentação oficial do Dapper no GitHub. Lá você encontra recursos como QueryMultiple, DynamicParameters e o suporte a stored procedures.

Quer aprender mais sobre Dapper

Se você gostou deste CRUD com Dapper e quer ir além, confira os conteúdos abaixo:

Conclusão

Neste artigo, montamos um CRUD com Dapper do zero, usando .NET 7 e SQL Server. Vimos como abrir a conexão, executar comandos com ExecuteAsync e ler dados com QueryAsync e QueryFirstOrDefaultAsync.

Além disso, você aprendeu a recuperar o Id inserido com SCOPE_IDENTITY e a tipar os resultados com records. Sobretudo, viu por que os parâmetros são a defesa básica contra SQL Injection.

Agora é praticar. Crie novas tabelas, adicione relacionamentos e repita o processo até que o CRUD com Dapper fique natural no seu dia a dia.

FAQ

O Dapper funciona só com SQL Server?

Não, ele funciona com qualquer provider ADO.NET. Isso inclui PostgreSQL, MySQL, MariaDB, SQLite, Oracle e Firebird.

Qual a diferença entre Execute e Query no Dapper?

O Execute roda comandos que não retornam linhas, como insert, update e delete. Já o Query executa consultas e mapeia o resultado para objetos.

Como pegar o Id gerado em um insert com Dapper?

Use SELECT CAST(SCOPE_IDENTITY() AS INT) logo após o insert. Em seguida, execute tudo com ExecuteScalarAsync<int>.

O Dapper protege contra SQL Injection?

Ele protege quando você usa parâmetros, como @id. Se você concatenar strings na instrução, a proteção deixa de existir.

Preciso abrir a conexão antes de usar o Dapper?

Não é obrigatório, porque o Dapper abre e fecha a conexão quando ela está fechada. Mesmo assim, você pode abri-la manualmente para reaproveitá-la em vários comandos.