Entity Framework Core vs Dapper: Um Comparativo de Performance

O comparativo EF Core vs Dapper é uma das discussões mais frequentes entre desenvolvedores .NET que trabalham com banco de dados. Neste artigo, vamos medir como os dois se comportam com diferentes quantidades de dados, usando o BenchmarkDotNet, e analisar o que os números realmente dizem.

O que são ORMs

Object-Relational Mappers, ou ORMs, são bibliotecas que facilitam o acesso a dados. Eles mapeiam as tabelas de um banco de dados para classes, e as instâncias dessas classes recebem as informações armazenadas no banco.

Na prática, o ORM elimina boa parte do código repetitivo de abrir conexão, executar comandos e converter linhas em objetos. Com isso, o desenvolvedor foca mais no domínio e menos na infraestrutura.

Micro ORM e full ORM

Os ORMs se dividem em dois grupos: micro ORM e full ORM. Os do primeiro grupo são conhecidos por serem leves e rápidos, enquanto os do segundo são ferramentas mais completas, com maior nível de abstração.

O Dapper é o exemplo clássico de micro ORM. Ele estende a interface IDbConnection com métodos como Query e QueryMultiple, mas deixa o SQL inteiramente nas suas mãos.

Já o Entity Framework Core é um full ORM. Além de mapear resultados, ele traduz LINQ para SQL, controla o rastreamento de alterações, gera migrações e carrega relacionamentos automaticamente.

Por isso, a comparação EF Core vs Dapper não é apenas sobre velocidade. Ela também envolve produtividade, controle sobre o SQL e custo de manutenção.

Se quiser ver as duas ferramentas lado a lado em um contexto mais amplo, confira o artigo sobre Dapper e Entity Framework Core.

Como funciona o benchmark EF Core vs Dapper

O teste realiza apenas operações de leitura com os dois ORMs. Para medir os resultados, ele usa o BenchmarkDotNet, a biblioteca de referência para benchmarks no ecossistema .NET.

As classes de teste usam os atributos [ShortRunJob], [RankColumn], [MaxColumn] e [MemoryDiagnoser]. Assim, o relatório mostra tempo médio, tempo máximo, ranking entre os métodos e memória alocada.

Domínio e cenários do teste

O domínio tem dois relacionamentos 1:N. Uma escola tem muitos professores, e um professor tem muitos alunos.

Em todos os cenários, cada escola tem 10 professores e cada professor tem 10 alunos. Em seguida, o número de escolas cresce de 10 para 100, 1.000 e 10.000.

Dessa forma, o total de entidades carregadas vai de 1.110 a 1.110.000. Esse crescimento mostra como o EF Core vs Dapper se comporta quando o volume aumenta dez vezes a cada etapa.

Consultas do Entity Framework Core

O Entity Framework usa eager loading, ou seja, carrega escolas, professores e alunos na mesma operação. Nesse lado do EF Core vs Dapper, o teste avalia três variações: sem ajuste, com AsSplitQuery e com AsNoTracking.

// Variação sem ajuste: uma única consulta com JOINs
var schools = context.Schools
    .Include(x => x.Teachers)
        .ThenInclude(x => x.Students)
    .ToList();

// Variação com AsNoTracking: sem rastreamento de alterações
var schoolsNoTracking = context.Schools
    .Include(x => x.Teachers)
        .ThenInclude(x => x.Students)
    .AsNoTracking()
    .ToList();

// Variação com AsSplitQuery: uma consulta SQL por coleção incluída
var schoolsSplit = context.Schools
    .Include(x => x.Teachers)
        .ThenInclude(x => x.Students)
    .AsSplitQuery()
    .ToList();

O AsNoTracking desliga o rastreamento de alterações, recomendado pela documentação para cenários somente leitura. Por outro lado, o AsSplitQuery troca os JOINs por consultas separadas para cada coleção incluída.

Consultas do Dapper

Com o Dapper, o teste usa duas abordagens: o método QueryMultiple e o método Query com uma única consulta. A primeira executa três SELECT em uma ida ao banco e monta os relacionamentos em memória.

using var connection = new SqlConnection(Settings.ConnectionString);

var query = @"
    SELECT * FROM Schools;
    SELECT * FROM Students;
    SELECT * FROM Teachers;";

var results = connection.QueryMultiple(query);

var schools = results.Read<SchoolModel>();
var students = results.Read<StudentModel>();
var teachers = results.Read<TeacherModel>();

// Monta os relacionamentos em memória
foreach (var school in schools)
    school.Teachers = teachers.Where(x => x.SchoolId == school.Id).AsList();

foreach (var teacher in teachers)
    teacher.Students = students.Where(x => x.TeacherId == teacher.Id).AsList();

A segunda abordagem faz um único SELECT com dois INNER JOIN. Em seguida, usa o recurso de multi mapping do Dapper, com splitOn, e dicionários para remover duplicidades.

Para quem ainda não usou a biblioteca, o artigo criando um CRUD com Dapper mostra o básico antes de partir para cenários de performance.

EF Core vs Dapper com 1.110 entidades

Neste primeiro cenário, temos 10 escolas, cada uma com 10 professores, e cada professor com 10 alunos. É o volume típico de uma tela de listagem ou de um relatório pequeno.

Tempo de execução no cenário 1

Entre os métodos do Entity Framework, o mais rápido foi o que usou AsSplitQuery, com 5,147 milissegundos. Do lado do Dapper, o QueryMultiple venceu com 0,764 milissegundo, ou seja, menos de 1 milissegundo.

Gráfico de barras do benchmark EF Core vs Dapper com 1.110 entidades comparando o tempo médio de execução do melhor método de cada ORM
Com poucos dados, o Dapper com QueryMultiple foi várias vezes mais rápido.

Consumo de memória no cenário 1

No Entity Framework, o método que menos alocou memória foi o AsNoTracking, com 963,26 KB. No Dapper, o relatório completo mostra o QueryMultiple com 219,07 KB e a consulta única com Query com 1.059,21 KB.

O gráfico abaixo compara o EF Core com o valor de 1.059,21 KB do Dapper. Contudo, pelo relatório do BenchmarkDotNet, o QueryMultiple alocou ainda menos memória neste cenário.

Gráfico de barras com o consumo de memória em KB do Entity Framework Core e do Dapper no cenário de 1.110 entidades
O gráfico usa o valor da consulta única do Dapper, e não o do QueryMultiple.

EF Core vs Dapper com 11.100 entidades

Neste cenário, temos 100 escolas, cada uma com 10 professores, e cada professor com 10 alunos. Aqui o volume já se aproxima de uma exportação ou de uma tela sem paginação.

Tempo de execução no cenário 2

Entre os métodos do Entity Framework, o mais rápido foi o AsNoTracking, com 34,23 milissegundos. Do lado do Dapper, o QueryMultiple ficou à frente com 32,53 milissegundos.

Repare como a diferença do EF Core vs Dapper praticamente desaparece neste ponto. Enquanto isso, a consulta sem ajuste do EF Core levou 75,66 milissegundos, mais que o dobro.

Gráfico de barras do benchmark EF Core vs Dapper com 11.100 entidades mostrando tempos de execução muito próximos
Com 11.100 entidades, AsNoTracking e QueryMultiple ficaram praticamente empatados.

Consumo de memória no cenário 2

No Entity Framework, o AsNoTracking alocou menos memória, com 8,61 MB. No Dapper, o QueryMultiple foi o melhor, com apenas 2,18 MB alocados, e a diferença de memória entre EF Core vs Dapper começa a ficar evidente.

Gráfico de barras com a memória alocada em MB pelo Entity Framework Core e pelo Dapper no cenário de 11.100 entidades
O Dapper alocou cerca de um quarto da memória usada pelo EF Core.

EF Core vs Dapper com 111.000 entidades

Neste cenário, temos 1.000 escolas, cada uma com 10 professores, e cada professor com 10 alunos. A partir daqui, o resultado muda de forma surpreendente.

Tempo de execução no cenário 3

Entre os métodos do Entity Framework, o mais rápido foi o AsNoTracking, com 312,9 milissegundos. Do lado do Dapper, o QueryMultiple venceu entre os dois métodos, mas levou 4,662 segundos, ou seja, pouco mais de 4 segundos e meio.

Em outras palavras, o EF Core foi cerca de 15 vezes mais rápido neste volume. Mais adiante, vamos entender por que isso aconteceu.

Gráfico de barras do benchmark EF Core vs Dapper com 111.000 entidades em que o EF Core aparece muito mais rápido
Com 111.000 entidades, a ordem se inverte e o EF Core assume a liderança em tempo.

Consumo de memória no cenário 3

No Entity Framework, o AsNoTracking alocou menos memória, com 85,25 MB. No Dapper, o QueryMultiple foi o melhor, com 21,34 MB alocados.

Gráfico de barras com a memória alocada em MB pelo Entity Framework Core e pelo Dapper no cenário de 111.000 entidades
Mesmo mais lento, o Dapper manteve a menor alocação de memória.

EF Core vs Dapper com 1.110.000 entidades

Neste último cenário, temos 10.000 escolas, cada uma com 10 professores, e cada professor com 10 alunos. Esse volume representa uma carga massiva, como uma migração de dados.

Tempo de execução no cenário 4

Entre os métodos do Entity Framework, o mais rápido foi o AsNoTracking, com 2,457 segundos, quase 2 segundos e meio. Do lado do Dapper, o QueryMultiple foi o mais rápido, mas levou 644,6 segundos, ou seja, mais de 10 minutos.

A consulta única com Query foi ainda pior e passou de 1.380 segundos. Portanto, neste volume a distância entre EF Core vs Dapper chega a duas ordens de grandeza.

Gráfico de barras com o tempo de execução do Entity Framework Core e do Dapper no cenário de 1.110.000 entidades
Com mais de um milhão de entidades, o tempo do Dapper passou de 10 minutos.

Consumo de memória no cenário 4

No Entity Framework, o AsNoTracking alocou menos memória, com 851,77 MB. No Dapper, o QueryMultiple foi o melhor, com 209,09 MB alocados.

Gráfico de barras com a memória alocada em MB pelo Entity Framework Core e pelo Dapper no cenário de 1.110.000 entidades
A vantagem do Dapper em memória se manteve em todos os volumes.

Análise dos resultados de EF Core vs Dapper

Os números do EF Core vs Dapper mostram dois padrões claros. Primeiro, o Dapper alocou menos memória em todos os cenários com o QueryMultiple; em seguida, ele perdeu feio em tempo nos dois cenários maiores.

Esse segundo resultado contraria a percepção comum de que o Dapper é sempre mais rápido. Por isso, vale olhar o código com atenção antes de tirar conclusões.

Por que o Dapper perdeu nos cenários maiores

O Dapper apenas executa o SQL e materializa os objetos. A montagem dos relacionamentos, porém, fica por conta do código da aplicação.

No teste, cada escola percorre a lista inteira de professores com Where, e cada professor percorre a lista inteira de alunos. Assim, o custo cresce de forma quadrática: com 100.000 professores e 1.000.000 de alunos, o laço faz cerca de 100 bilhões de comparações.

Uma alternativa seria indexar os filhos antes de montar o grafo, por exemplo com ToLookup. O trecho abaixo é uma sugestão de melhoria e não fez parte das medições.

// Sugestão: indexa professores e alunos pela chave estrangeira
var teachersBySchool = teachers.ToLookup(x => x.SchoolId);
var studentsByTeacher = students.ToLookup(x => x.TeacherId);

foreach (var school in schools)
    school.Teachers = teachersBySchool[school.Id].ToList();

foreach (var teacher in teachers)
    teacher.Students = studentsByTeacher[teacher.Id].ToList();

Com isso, cada busca passa a ter custo constante. Em outras palavras, a diferença do EF Core vs Dapper nos cenários grandes reflete mais a montagem em memória do que o acesso ao banco.

O peso do AsNoTracking e do AsSplitQuery

No EF Core, o AsNoTracking venceu em tempo do cenário 2 em diante e em memória em todos os cenários. Isso faz sentido, porque o rastreamento mantém um dicionário de instâncias e um snapshot de cada entidade.

A documentação de consultas eficientes do EF Core explica que, em cenários somente leitura, esse custo extra pode ser evitado. Nesse caso, desligar o rastreamento é quase sempre o primeiro ajuste.

Já o AsSplitQuery só venceu no cenário pequeno. Segundo a documentação, consultas divididas evitam a explosão cartesiana, mas exigem uma ida ao banco por consulta e mantêm resultados em memória.

Vale lembrar que o modelo usa ThenInclude, ou seja, as coleções estão em níveis diferentes. Nesse formato, a documentação explica que o produto cartesiano não ocorre, o que reduz a vantagem do split query.

Limitações do benchmark EF Core vs Dapper

O atributo [ShortRunJob] executa poucas iterações, então a margem de erro de alguns métodos ficou alta. Por exemplo, a consulta única do Dapper no cenário 1 teve erro maior que a própria média.

Além disso, os testes rodaram em um notebook, com SQL Server local. Portanto, trate os números do EF Core vs Dapper como tendência, e não como valor absoluto para produção.

Quando escolher EF Core vs Dapper

Ao comparar os ORMs com diferentes quantidades de dados, surgem perguntas que passam despercebidas no primeiro momento. O que é mais importante, velocidade ou alocação de memória?

A ferramenta está sendo usada da melhor maneira possível? Com a quantidade de dados que a aplicação processa, faz sentido trocar de ORM?

Na prática, a escolha entre EF Core vs Dapper depende do que você precisa otimizar.

O Dapper brilha quando você quer controle total do SQL e baixa alocação. Por outro lado, ele exige que você mesmo resolva os relacionamentos, e um laço mal escrito anula a vantagem.

O EF Core, por sua vez, entrega produtividade e resolve o grafo de objetos de forma eficiente. Com AsNoTracking, ele fica muito competitivo em leitura, como o próprio benchmark mostrou.

Também é possível combinar os dois no mesmo projeto. Por exemplo, muitos times usam EF Core para escrita e Dapper para consultas pesadas de relatório.

Se o seu gargalo for escrita em massa, e não leitura, o artigo sobre bulk insert com EF Core apresenta outras estratégias.

Ambiente do teste EF Core vs Dapper

O benchmark rodou com as seguintes ferramentas e versões:

  • .NET 8.0
  • BenchmarkDotNet 0.13.12
  • Dapper 2.1.35
  • Microsoft.EntityFrameworkCore 8.0.4
  • Microsoft.EntityFrameworkCore.SqlServer 8.0.4
  • System.Data.SqlClient 4.8.6
  • Banco de dados SQL Server 2022
  • Sistema operacional Windows 11 23H2

O computador usado tem as seguintes especificações:

  • Processador Intel Core i5-10210U de 1,6 GHz
  • Memória de 24 GB a 2667 MHz
  • Disco ADATA SX6000PNP, SSD de 256 GB

O código usado e o resultado completo do BenchmarkDotNet estão neste repositório no GitHub. Assim, você pode reproduzir os testes ou ajustar o código para o seu cenário.

Conclusão

Cenários diferentes exigem estratégias diferentes, e as duas ferramentas têm seu lugar. O Dapper alocou menos memória em todos os volumes, enquanto o EF Core com AsNoTracking foi mais rápido nos cenários grandes.

No fim, o comparativo EF Core vs Dapper mostra que a forma de usar a ferramenta pesa tanto quanto a ferramenta em si. Por isso, meça sempre com dados próximos da realidade antes de trocar de ORM.

FAQ

O Dapper é sempre mais rápido que o EF Core?

Não. Neste benchmark, o Dapper venceu com poucos dados, mas o EF Core com AsNoTracking foi muito mais rápido a partir de 111.000 entidades, principalmente por causa da montagem dos relacionamentos em memória.

Qual ORM consome menos memória?

O Dapper com QueryMultiple alocou menos memória em todos os cenários do teste. No cenário com 1.110.000 entidades, foram 209,09 MB contra 851,77 MB do EF Core com AsNoTracking.

Quando devo usar AsNoTracking?

Use em consultas somente leitura, quando você não vai alterar e salvar as entidades retornadas. Assim, o EF Core evita o custo de rastreamento e de snapshot.

O AsSplitQuery sempre melhora a performance?

Não. Ele ajuda quando há explosão cartesiana, mas adiciona idas ao banco e, neste teste, só foi o melhor método do EF Core no menor cenário.

Posso usar EF Core e Dapper no mesmo projeto?

Sim. É comum usar o EF Core para escrita e regras de domínio, e o Dapper para consultas de leitura mais pesadas.

Glossário

Termo Significado
ORM Biblioteca que mapeia tabelas do banco de dados para classes da aplicação
Micro ORM ORM leve, focado em executar SQL e materializar objetos
Full ORM ORM completo, com tradução de consultas, rastreamento e migrações
Eager loading Carregamento antecipado das entidades relacionadas na mesma operação
AsNoTracking Método do EF Core que desliga o rastreamento de alterações na consulta
AsSplitQuery Método do EF Core que divide a consulta em um SQL por coleção incluída
QueryMultiple Método do Dapper que lê vários conjuntos de resultados de um único comando
Explosão cartesiana Multiplicação de linhas causada por JOINs entre coleções do mesmo nível
BenchmarkDotNet Biblioteca .NET para medir tempo e memória de trechos de código

Fontes