Entity Framework Core evolucionó hasta convertirse en un ORM potente y completo que permite a los desarrolladores .NET trabajar de forma eficiente con bases de datos relacionales sin sacrificar la claridad del código.
Con EF Core 8 en adelante, nuevas funcionalidades como compiled queries, aggregaciones avanzadas, split queries, soporte para columnas JSON, EF.Functions y filtros globales hacen que sea más fácil que nunca escribir código de acceso a datos de alto rendimiento, expresivo y mantenible.
En este post vamos a explorar ejemplos prácticos del mundo real para aprovechar al máximo estas herramientas, optimizar consultas, simplificar operaciones complejas y evitar errores comunes — sin perder legibilidad ni calidad de dominio.
1. Compiled Queries: Optimización en Rutas Críticas
En EF Core, cada consulta LINQ se traduce a SQL cada vez que se ejecuta. Para la mayoría de las aplicaciones esto está bien, pero en APIs de alto tráfico o rutas críticas de rendimiento, esa traducción puede volverse un cuello de botella.
Una compiled query es esencialmente una versión preprocesada de una consulta LINQ que EF Core guarda en memoria para no tener que traducirla a SQL cada vez que se ejecuta.
// Consulta LINQ estándar
var user = await db.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == userId);
// Versión compilada
private static readonly Func<AppDbContext, Guid, User?> _getUserById =
EF.CompileQuery(
(AppDbContext db, Guid id) =>
db.Users
.AsNoTracking()
.FirstOrDefault(u => u.Id == id));
// Uso
var userCompiled = _getUserById(db, userId);
✅ Reduce el overhead de la traducción LINQ→SQL en cada llamada.
✅ Especialmente útil en dashboards, lookups o entidades de acceso frecuente.
✅ Se puede combinar con AsNoTracking() para mayor rendimiento en lecturas.
💡 No uses compiled queries en todos lados — reservalas para rutas que se ejecutan cientos o miles de veces por segundo.
2. Tracking vs No-Tracking e Identity Resolution
EF Core permite cargar entidades con o sin tracking. El tracking es conveniente para actualizaciones, pero tiene un costo en memoria y CPU.
- Con tracking: EF Core recuerda cada entidad cargada. Si dos consultas devuelven la misma entidad (misma PK), te da la misma instancia de objeto.
- Sin tracking (
AsNoTracking()): EF Core no trackea entidades. Cada vez que consultas la misma entidad, se crea una nueva instancia, incluso si representa la misma fila de la base de datos.
El problema con AsNoTracking() en relaciones es que podés terminar con objetos duplicados. La solución: AsNoTrackingWithIdentityResolution().
var orders = await db.Orders
.AsNoTrackingWithIdentityResolution()
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Where(o => o.OrderDate >= DateTime.UtcNow.AddDays(-30))
.ToListAsync();
✅ Sin overhead de change tracking → rápido.
✅ Objetos consistentes: múltiples órdenes con el mismo producto comparten la misma instancia.
💡 Ideal para endpoints de lectura, reportes y dashboards.
3. Operaciones Bulk: ExecuteUpdate y ExecuteDelete
Las versiones anteriores de EF Core requerían cargar entidades en memoria, modificarlas y luego llamar a SaveChanges(). Para actualizaciones o eliminaciones masivas, esto es ineficiente.
EF Core 8+ introduce ExecuteUpdate y ExecuteDelete para operar directamente en la base de datos.
// Eliminar órdenes de más de un año
await db.Orders
.Where(o => o.OrderDate < DateTime.UtcNow.AddYears(-1))
.ExecuteDeleteAsync();
// Actualizar estado en bulk
await db.Orders
.Where(o => o.Status == "Pending" && o.OrderDate < DateTime.UtcNow.AddDays(-7))
.ExecuteUpdateAsync(o => o.SetProperty(order => order.Status, "Expired"));
✅ Elimina el overhead de cargar entidades en memoria.
✅ Reduce los round-trips a la base de datos.
✅ Perfecto para tareas de mantenimiento, jobs programados o background services.
💡 Estas operaciones bypasean el change tracking de EF Core, tenerlo en cuenta.
4. Aggregaciones Modernas con GroupBy
EF Core 8+ traduce Count, Sum, Min/Max, Average y GroupBy directamente a SQL, sin necesidad de traer datos a memoria.
// Estadísticas de órdenes por cliente
var customerStats = await db.Orders
.GroupBy(o => o.CustomerId)
.Select(g => new
{
CustomerId = g.Key,
OrdersCount = g.Count(),
TotalAmount = g.Sum(o => o.Total),
MinOrder = g.Min(o => o.Total),
MaxOrder = g.Max(o => o.Total),
AverageOrder = g.Average(o => o.Total)
})
.ToListAsync();
✅ Agregaciones server-side → menos datos transferidos y menor uso de memoria.
✅ Ideal para dashboards, analytics o APIs de reportes.
💡 Combinar con compiled queries si es una ruta de acceso frecuente.
5. Split Queries y Optimización de Include
Cuando se consultan entidades con múltiples tablas relacionadas (Include/ThenInclude), EF Core puede generar JOINs enormes que producen filas duplicadas y ralentizan las queries — conocido como cartesian explosion.
EF Core 8+ introduce AsSplitQuery() para ejecutar múltiples queries más pequeñas en lugar de un JOIN gigante.
var customers = await db.Customers
.AsNoTracking()
.AsSplitQuery()
.Include(c => c.Orders)
.ThenInclude(o => o.Items)
.ThenInclude(i => i.Product)
.Include(c => c.Addresses)
.Where(c => c.IsActive)
.ToListAsync();
✅ Reduce el tamaño y complejidad de las queries SQL.
✅ Evita cargar entidades duplicadas en memoria.
✅ Especialmente útil para relaciones profundamente anidadas.
💡 Para navegaciones simples (no colecciones), un JOIN normal generalmente es suficiente.
6. Interceptors y Logging
EF Core permite interceptar queries, comandos y operaciones de guardado, dándote visibilidad profunda sobre la actividad de la base de datos. Ideal para monitoreo de rendimiento o detectar problemas de N+1.
// Interceptor que loguea queries largas
public class QueryInterceptor : DbCommandInterceptor
{
public override async Task<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
if (command.CommandText.Length > 1000)
{
Console.WriteLine($"Long query detected: {command.CommandText}");
}
return await base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
}
// Query tagging para profiling
var activeCustomers = await db.Customers
.TagWith("Fetching active customers for dashboard")
.Where(c => c.IsActive)
.ToListAsync();
✅ Identificá queries lentas o problemáticas en producción.
✅ Aplicá políticas de auditoría automáticamente sin cambiar la lógica de negocio.
7. Columnas JSON y Datos Flexibles
EF Core 8+ permite mapear columnas JSON a propiedades, almacenando datos semi-estructurados con soporte para queries directas sobre el JSON.
// Filtrar por metadata JSON
var redProducts = await db.Products
.Where(p => p.Metadata["Color"].ToString() == "Red")
.ToListAsync();
✅ Schema flexible sin cambiar la estructura de la base de datos.
✅ Queryable directamente en SQL, sin traer todo el JSON a memoria.
✅ Ideal para catálogos de productos, atributos opcionales o configuraciones dinámicas.
8. Raw SQL y Mapeo a DTOs
Cuando LINQ no alcanza para JOINs complejos o funciones específicas de la base de datos, EF Core permite queries SQL crudas que mapean directamente a DTOs.
var topCustomers = await db.TopCustomers
.FromSqlInterpolated($@"
SELECT c.Id, c.Name, SUM(o.Total) as TotalSpent
FROM Customers c
JOIN Orders o ON c.Id = o.CustomerId
GROUP BY c.Id, c.Name
HAVING SUM(o.Total) > {minSpend}
")
.ToListAsync();
✅ Accedé a funcionalidades SQL complejas no soportadas aún en LINQ.
✅ Mapeo directo a DTOs para APIs o reportes.
💡 Siempre parametrizá el SQL crudo (o usá FromSqlInterpolated) para prevenir inyección SQL.
9. EF.Functions y SQL Helpers
EF Core expone funciones de la base de datos directamente en LINQ, permitiendo operaciones SQL eficientes sin escribir SQL crudo.
// Órdenes de los últimos 30 días
var recentOrders = await db.Orders
.Where(o => EF.Functions.DateDiffDay(o.OrderDate, DateTime.UtcNow) < 30)
.ToListAsync();
// Pattern matching con LIKE
var redProducts = await db.Products
.Where(p => EF.Functions.Like(p.Name, "%Red%"))
.ToListAsync();
// Búsqueda case-insensitive (PostgreSQL)
var redProductsIgnoreCase = await db.Products
.Where(p => EF.Functions.ILike(p.Name, "%red%"))
.ToListAsync();
// Tiendas cercanas (datos espaciales)
var nearbyStores = await db.Stores
.Where(s => EF.Functions.STDistance(s.Location, userLocation) < 1000)
.ToListAsync();
✅ Aprovecha funciones de la base de datos sin escribir SQL crudo.
✅ Sintaxis LINQ limpia que igual se ejecuta eficientemente en SQL.
10. Global Query Filters
Los filtros globales permiten filtrar entidades automáticamente a nivel del DbContext, sin tener que agregar .Where() en cada consulta. Casos de uso comunes: soft deletes, multi-tenancy o archivado.
// Configurar filtro global para soft delete
modelBuilder.Entity<User>()
.HasQueryFilter(u => !u.IsDeleted);
// Uso: automáticamente excluye usuarios eliminados
var activeUsers = await db.Users.ToListAsync();
✅ Garantiza filtrado consistente en todas las queries.
✅ Reduce cláusulas .Where() repetitivas en el código.
💡 Combinalo con interceptors o campos de auditoría para apps multi-tenant.
11. Enums y Value Conversions
EF Core permite mapear enums u objetos de valor personalizados a columnas de la base de datos con conversiones, manteniendo un modelo de dominio expresivo.
// Mapear enum como string
modelBuilder.Entity<Order>()
.Property(o => o.Status)
.HasConversion<string>();
// Mapear value object personalizado
modelBuilder.Entity<User>()
.Property(u => u.Email)
.HasConversion(
v => v.Value, // guardar como string
v => new Email(v) // recuperar como objeto Email
);
✅ Modelos de dominio type-safe con persistencia simple en la base de datos.
✅ Valores legibles en la base de datos sin sacrificar la expresividad del dominio.
12. Errores Comunes y Trampas a Evitar
Incluso desarrolladores experimentados pueden caer en estas trampas:
- N+1 Queries: Si olvidás el
Include()para entidades relacionadas, EF Core puede lanzar una query por cada fila padre. UsáInclude()con criterio, oAsSplitQuery()para múltiples colecciones.
- Client-Side Evaluation: EF Core intenta traducir LINQ a SQL, pero cuando no puede, evalúa en el cliente trayendo todos los datos a memoria. Revisá los warnings y evitalo en rutas críticas.
- Overhead de Tracking: Por defecto EF Core trackea entidades, lo que es conveniente para actualizaciones pero agrega overhead. Para operaciones de solo lectura, usá
AsNoTracking()oAsNoTrackingWithIdentityResolution().
- Riesgos de Raw SQL: Siempre parametrizá las queries con
FromSqlInterpolated()para prevenir inyección SQL.
- Cambios de versión: EF Core 8+ introduce nuevas reglas de traducción para aggregaciones,
GroupByy otras queries. Código que funcionaba en EF Core 7 puede comportarse diferente — siempre testeá después de upgrades.
💡 Usá logging, interceptors y query tagging para detectar estos problemas temprano. Ver el SQL exacto que genera tu LINQ hace que debuggear problemas de rendimiento sea mucho más fácil.
Conclusión
EF Core moderno le da a los desarrolladores las herramientas para manejar desde aggregaciones complejas y relaciones profundas hasta queries críticas de rendimiento y datos JSON semi-estructurados — manteniendo el código limpio, expresivo y mantenible.
Combinando compiled queries, split queries, EF.Functions, filtros globales y value conversions, podés construir aplicaciones robustas y de alto rendimiento que escalan con gracia y minimizan el uso de memoria.
¡Happy coding! 🚀