Uno de los grandes debates de este nuestro nicho de C# es si debemos utilizar Dapper o de entity framework a la hora de hacer llamadas a la base de datos.
Índice
1 - Qué es Dapper y Entity Framework Core?
Dentro de C# Tenemos diferentes formas de conectarnos a una base de datos relacional, la más común es utilizar librerías, y las más comunes son Dapper
y Entity Framework Core
.
Para ambas librerías tengo contenido en este canal.
Dapper es un micro ORM que traduce la información devuelta de la base de datos a objetos dentro de C#. Enlace al post.
Entity Framework es una librería ORM creada y mantenida por Microsoft, la mucho más completa y más grande que contiene muchas más funcionalidades que Dapper. Enlace al curso de Entity framework.
Las funcionalidades de Dapper también están disponibles dentro de EF Core, además de muchas otras.
1.1- ¿Por qué hay tantas dudas sobre utilizar Dapper o Entity Framework?
La respuesta a esta pregunta viene a raíz de que en versiones antiguas de Entity Framework sobre todo a las versiones anteriores a NET 5 el rendimiento de Entity framework era muy lento a la hora de hacer consultas. No para todo tipo de consultas, pero cuando había que consultar múltiples tablas el SQL que entity framework generaba por detrás no era el más eficiente, de hecho lo contrario, solía ser bastante malo.
Así, que pese a los beneficios que trae Entity Framework, como pueden ser las migraciones, el traqueo de entidades, los interceptores, etc. Muchas de las empresas preferían no utilizarlo y utilizar Dapper o incluso el conector de la base de datos que utilicen directamente.
Desde NET 5, esta parte del rendimiento ha mejorado y se supone que está a la par, o muy similar a Dapper, que es lo que vamos a comprobar en este post.
De hecho en la netconf de 2021 dijeron que previamente Dapper era un 55% más rápido que EF, pero que a partir de esa versión era de un 5%.
Esta diferencia es bastante elevada, pero en muchos casos “da igual”, si una app recibe una llamada al minuto te da igual que tarde 30ms en responder que 50, pero claro, si en vez de dos llamadas por minuto tienes varios miles, esos milisegundos si importan.
2 - Construcción del escenario para testear Dapper y Entity Framework.
Para este testeo no me voy a complicar demasiado y la arquitectura va a ser la siguiente:
- Base de datos PostgreSQL Corriendo en Docker.
- Applicación la cual utiliza la base de datos
- Proyecto de test que invocará la base de datos a través de la capa de la API (sí, llamadas API).
Aprovechando que ya tengo gran parte del código escrito, vamos a utilizar mi proyecto de GitHub del curso de entity framework Core, donde tenemos dos entidades, la entidad User con el Email el nombre y el Id y la entidad WorkingExperience con el Id y varios campos. La relación es 1-n donde un usuario puede tener múltiples WorkingExperiences.
Si quieres mas detalles sobre el uso de Entity Framework en el mundo real, puedes comprar mi libro, la Guía completa de desarrollo full stack con .NET.
2.1 - Ejecución de los tests
No vamos a introducir configuración extra como podría ser añadir interceptores en la caché de Entity Framework, lo que sí que vamos a hacer es intentar replicar un escenario de producción real.
Por ejemplo, en Entity framework lo haremos a través de la entidad del dbContext (lo que requiere leer primero) y el Unit of work ya que en el mundo real es como las empresas lo hacen.
De la misma forma, he añadido código para que Dapper sea capaz de hacer transacciones, que obviamente esto hace que el resultado sea un poquito más lento con respecto a hacer llamadas planas, pero es más acorde a lo que sería una aplicación real en producción.
Finalmente, el test no es exclusivo con la comunicación a la base de datos, sino que lo ejecutaré contra la API, ambas opciones tienen el mismo código y el resultado debería ser concluyente. Si tienes dudas de cómo está montado todo, el código lo tienes a total disposición en GitHub.
Paquetes a utilizar y versiones:
Package | version |
Npgsql.EntityFrameworkCore.PostgreSQL | 9.0.4 |
Microsfot.EntityFrameworkCore | 9.0.1 |
Dapper | 2.1.66 |
2.2 - Test de inserción
Para insertar, y ahorrarme tiempo, simplemente le pasamos al caso de uso un id, el cual se utilizará para identificar de una forma más sencilla, pero todo los datos son hardcoded para simplificar el proceso.
Este es el escenario de Entity Framework
public class InsertUser(IUnitOfWork unitOfWork)
{
public async Task<User> Execute(int id)
{
User user = new User()
{
Email = $"{Guid.NewGuid()}@mail.com",
UserName = $"id{id}"
};
List<Wokringexperience> workingExperiences =
[
new()
{
User = user,
Name = $"experience1 user {id}",
Details = "details1",
Environment = "environment"
},
new()
{
User = user,
Name = $"experience user {id}",
Details = "details2",
Environment = "environment"
}
];
user = await unitOfWork.UserRepository.Insert(user); // 👈
await unitOfWork.WorkingExperienceRepository.Insert(workingExperiences); // 👈
_ = await unitOfWork.Save();// 👈
return user;
}
}
En el caso de dapper, hacemos lo mismo:
public class InsertUserDapper(UoWDapper unitOfWork)
{
public async Task<UserDto> Execute(int id)
{
await unitOfWork.OpenTransaction(); // 👈
UserDto userDto = await unitOfWork.UserDapperRepository.InsertSingle(new UserDto()
{
Email = $"{Guid.NewGuid()}@mail.com",
UserName = $"id{id}"
});
List<WorkingExperienceDto> workingExperiences =
[
new()
{
UserId = userDto.Id,
Name = $"experience1 user {id}",
Details = "details1",
Environment = "environment"
},
new()
{
UserId = userDto.Id,
Name = $"experience user {id}",
Details = "details2",
Environment = "environment"
}
];
List<WorkingExperienceDto> updatedExperiences = await unitOfWork
.WorkingExperienceDapperRepository
.InsertList(workingExperiences); // 👈
await unitOfWork.CommitTransaction(); // 👈
userDto.WorkingExperiences = updatedExperiences;
return userDto;
}
}
Y los resultados son muy similares:
Como vemos el insertar con Dapper es ligeramente más rápido que hacerlo con Entity Framework Core; Aunque en este proceso estamos llamando a una API no haciendo únicamente la inserción ya que coomo digo, intento replicar una situación real.
Nota: cada test crea unos 3.5 mil registros en la base de datos de usuarios y unos 7 mil en la de experiencias.
2.3 - Test de lectura
Igual que el caso anterior, simplemente leemos los resultados.
Código de Entity Framework:
public class GetUser(IUnitOfWork unitOfWork)
{
public async Task<User?> Execute(int id)
{
return await unitOfWork.UserRepository.GetByIdWithWorkingExperiences(id);
}
}
public async Task<User?> GetByIdWithWorkingExperiences(int id)
=> await Entities
.Include(a => a.Wokringexperiences)
.FirstOrDefaultAsync(x => x.Id == id);
Código de Dapper:
public class GetUserDapper(UoWDapper unitOfWork)
{
public async Task<UserDto?> Execute(int id)
{
return await unitOfWork.UserDapperRepository.GetById(id);
}
}
public async Task<UserDto?> GetById(int id)
{
DbConnection connection = await _transaction.GetConnectionAsync();
string sql = "select u.*, w.* " +
" from users u " +
" inner join workingexperiences w on u.id = w.userid " +
" where u.id = @id";
UserDto? user = null;
await connection.QueryAsync<UserDto, WorkingExperienceDto, UserDto>(
sql,
(userResult, workingExperience) =>
{
if (user == null)
{
user = userResult;
user.WorkingExperiences = new List<WorkingExperienceDto>();
}
if (workingExperience != null)
{
user.WorkingExperiences.Add(workingExperience);
}
return user;
},
new { id },
splitOn: "id"
);
return user;
}
Y el resultado:
Podemos ver como Dapper es ligeramente más rápido, pero no mucho, lo cual es lo esperado, ya que Dapper simplemente traduce la respuesta de SQL a un objeto en C#.
3 - Podemos utilizar tanto Dapper como EF Core?
Una pregunta que puede ser que te estés haciendo es si podemos utilizar la combinación de ambos, utilizamos EF Core para todo lo que tiene que ver con inserciones, actualización y borrado, ya que la velocidad es prácticamente la misma y luego Entity framework nos da otra cantidad de funcionalidades extra, como pueden ser los interceptores, El patrón repositorio y Unit Of Work, facilidad de uso, etc.
Y por otro lado, Dapper es más rápido a la hora de leer.
La respuesta es que sí. Ahora, en mi opinión no merece la pena ya que ambos tienen configuración específica del mismo y siempre es un engorro tener que actualizar varios elementos y configuraciones.
Mi recomendación es saber cuándo tenemos que utilizar cada uno, si tienes una app que recibe un par de llamadas por minuto, Dapper no te va a dar una ventaja o un beneficio que pueda justificar el reemplazo de EF.
En cambio, si tienes una app que recibe 100mil llamadas por minuto, y el 99% son de lectura, en ese caso Dapper te va a dar una mejora en el rendimiento con respecto a Entity framework.