Entity Framework Core en 2026: mejoras, patrones modernos y cuándo no usarlo

Si trabajas con .NET, EF Core es probablemente el ORM que tienes instalado en tu proyecto. No porque sea el único, sino porque es el que viene de serie con ASP.NET Core, encaja con Minimal APIs y Blazor, y tiene detrás a Microsoft con actualizaciones periódicas. EF Core 9 salió en noviembre de 2024 junto con .NET 9, y EF Core 10 llegó en noviembre de 2025 con .NET 10.

Las alternativas existen y tienen su sitio: Dapper te da más control sobre el SQL que se ejecuta con menos abstracción, NHibernate sigue siendo una opción para proyectos legacy que lo usan desde hace años, y Marten es interesante si trabajas con PostgreSQL y quieres una base de datos de documentos. Pero si empiezas un proyecto nuevo hoy, EF Core es el punto de partida lógico.

En este artículo repaso las novedades más útiles de EF Core 9, los patrones que más se usan en proyectos reales y los casos donde tiene más sentido bajarse del ORM y escribir SQL a mano.

ExecuteUpdateAsync y ExecuteDeleteAsync: operaciones masivas sin cargar entidades

Este es el cambio que más impacto tiene en el rendimiento del día a día. Llegó en EF Core 7 y sigue siendo una de las cosas que más sorprenden a quien lo descubre tarde.

El problema del enfoque clásico: para eliminar registros, EF Core primero los cargaba en memoria y luego los borraba uno a uno.

// Antes: carga todos en memoria, luego borra
var users = context.Users.Where(u => !u.Active).ToList();
context.Users.RemoveRange(users);
await context.SaveChangesAsync();

Con mil registros ya empieza a ser lento. Con cien mil, un problema serio. Desde EF Core 7 puedes hacer esto:

// Un único DELETE en la base de datos
await context.Users
    .Where(u => !u.Active)
    .ExecuteDeleteAsync();

EF Core traduce eso a un DELETE FROM Users WHERE Active = 0 directo. Sin cargar nada en memoria, sin change tracking, sin SaveChanges. Lo mismo con actualizaciones:

await context.Orders
    .Where(o => o.Status == "Pending")
    .ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, "Cancelled"));

Puedes encadenar varios SetProperty en la misma llamada para actualizar múltiples columnas en un solo UPDATE. EF Core 9 mejoró el soporte para update statements más complejos con esta API.

AsNoTracking: consultas de solo lectura más rápidas

Por defecto, EF Core rastrea todas las entidades que carga. Mantiene un registro interno de su estado para detectar cambios cuando llamas a SaveChanges. Eso tiene un coste en memoria y en tiempo, y en muchos casos es completamente innecesario.

Si solo vas a leer datos para mostrarlos en una API o en una página, no necesitas ese tracking:

var products = await context.Products
    .AsNoTracking()
    .Where(p => p.Active)
    .ToListAsync();

Menos memoria, menos procesamiento, más velocidad. Para endpoints de lectura en una API REST, debería ser el comportamiento por defecto.

Existe también AsNoTrackingWithIdentityResolution(): sin change tracking igual que el anterior, pero EF Core se encarga de devolver la misma instancia de objeto cuando la misma entidad aparece varias veces en los resultados. Útil cuando tienes relaciones donde el mismo registro puede aparecer referenciado desde distintos lados.

Lazy loading vs eager loading vs explicit loading

Una de las decisiones que más afecta al rendimiento de las consultas es cómo cargas las relaciones. Hay tres opciones y no todas son igual de recomendables.

Eager loading

Cargas la entidad principal y sus relaciones en una sola query con JOINs:

var orders = await context.Orders
    .Include(o => o.Items)
    .ThenInclude(i => i.Product)
    .ToListAsync();

Es predecible, genera SQL claro y es la opción más segura para la mayoría de los casos en APIs web.

Lazy loading

Las relaciones se cargan automáticamente cuando accedes a la propiedad de navegación. Para activarlo necesitas instalar Microsoft.EntityFrameworkCore.Proxies y configurarlo. El problema es que puede generar el problema N+1 sin que te des cuenta: si tienes diez pedidos y accedes a los ítems de cada uno en un bucle, EF Core lanza diez queries a la base de datos en vez de una.

Hay casos donde puede tener sentido, pero en aplicaciones web hay que tener muy claro lo que hace. Si no controlas el SQL que se genera, mejor evitarlo.

Explicit loading

Cargas las relaciones de forma manual cuando las necesitas:

var order = await context.Orders.FindAsync(orderId);
await context.Entry(order).Collection(o => o.Items).LoadAsync();

Más verboso que el lazy loading, pero completamente controlado. Sabes exactamente cuándo se lanza cada query.

Para la mayoría de APIs web: eager loading o proyecciones. El lazy loading sin entender bien cómo funciona es la fuente de muchos problemas de rendimiento que son difíciles de diagnosticar.

Proyecciones a DTO: solo carga lo que necesitas

Cargar entidades completas cuando solo necesitas tres campos es uno de los errores más comunes. EF Core puede traducir un Select a SQL y traer únicamente las columnas que pides:

var summaries = await context.Orders
    .Select(o => new OrderSummaryDto
    {
        Id = o.Id,
        Total = o.Total,
        CustomerName = o.Customer.Name
    })
    .ToListAsync();

EF Core genera un SELECT con solo esas tres columnas, hace el JOIN necesario para Customer.Name y no toca nada más. Si en vez de eso haces .Include(o => o.Customer).ToListAsync() y luego mapeas manualmente, estás trayendo todas las columnas de pedidos y clientes aunque solo uses tres.

Las proyecciones también eliminan la necesidad de configurar el change tracking: EF Core no rastrea instancias de DTOs anónimos ni de clases que no son entidades del contexto.

Migraciones: el flujo de trabajo correcto

Las migraciones son la forma de mantener la base de datos sincronizada con el modelo de C#. El flujo básico:

# Crear una migración nueva
dotnet ef migrations add NombreMigracion

# Ver el SQL que se va a ejecutar antes de aplicar
dotnet ef migrations script

# Aplicar las migraciones pendientes al entorno local
dotnet ef database update

El paso del script es importante: EF Core a veces genera SQL que hace cosas que no esperabas, sobre todo en migraciones que renombran columnas o cambian tipos. Revisar el SQL antes de aplicarlo en producción evita sustos.

Para aplicar migraciones automáticamente al arrancar la aplicación en producción:

await dbContext.Database.MigrateAsync();

Funciona, pero hay que usarlo con criterio. En aplicaciones con múltiples instancias, todas intentarán migrar al mismo tiempo. Para ese escenario es mejor un proceso de migración separado o un job que corra antes del despliegue.

EF Core 9: las novedades que más se usan

HierarchyId para datos jerárquicos en SQL Server

EF Core 9 añadió soporte nativo para el tipo hierarchyid de SQL Server. Es útil para estructuras de árbol como categorías anidadas, organigramas o comentarios con respuestas. Antes había que gestionarlo con columnas de ParentId y queries recursivas; ahora puedes usar el tipo directamente en el modelo y EF Core sabe cómo mapearlo.

LINQ mejorado: más traducción a SQL

Cada versión de EF Core amplía el conjunto de operaciones LINQ que se traducen a SQL en vez de evaluarse en el cliente. En EF Core 9 mejoraron bastante el soporte de GroupBy con operaciones agregadas más complejas. Antes, ciertas combinaciones de GroupBy con Sum, Count o Average forzaban una evaluación en cliente que podía traer miles de registros sin que lo vieras.

Vale la pena revisar el SQL que genera EF Core con ToQueryString() o con el logging de EF Core, especialmente en queries con agrupaciones:

var query = context.Orders
    .Where(o => o.Status == "Completed")
    .GroupBy(o => o.CustomerId)
    .Select(g => new { CustomerId = g.Key, Total = g.Sum(o => o.Total) });

// Ver el SQL antes de ejecutar
Console.WriteLine(query.ToQueryString());

FillFactor y column ordering en índices

EF Core 9 permite configurar el FillFactor de los índices en SQL Server directamente desde la configuración del modelo, lo que da más control sobre cómo se reserva espacio para inserciones futuras. También permite especificar el orden de las columnas en índices compuestos, algo que antes requería SQL raw en las migraciones.

Cuándo no usar EF Core y optar por Dapper o SQL directo

EF Core cubre bien el CRUD estándar, pero hay casos donde añade más problemas de los que resuelve.

  • Queries con muchos JOINs y optimizaciones específicas: EF Core puede generar SQL subóptimo en consultas complejas. Si el DBA tiene un query afinado que tarda 50ms y EF Core genera uno que tarda 2 segundos, la abstracción no vale la pena.
  • Inserciones masivas de cientos de miles de registros: ExecuteUpdate y ExecuteDelete mejoran mucho las operaciones por lotes, pero para bulk inserts de gran volumen, SqlBulkCopy o extensiones como EFCore.BulkExtensions son más adecuados.
  • Proyectos donde el SQL lo escribe el DBA: Si el modelo de trabajo es que el DBA define los stored procedures o las views y el desarrollador solo los llama, Dapper encaja perfectamente. No necesitas el overhead de EF Core para ejecutar un SELECT * FROM vw_ReporteMensual.
  • Reporting y agregaciones complejas: Para queries de tipo BI con muchas agrupaciones, subqueries y window functions, escribir el SQL a mano con Dapper da más control y el resultado suele ser más predecible.

Lo habitual en proyectos .NET modernos es combinar los dos: EF Core para el CRUD de entidades del dominio, Dapper o SQL raw para las queries analíticas o las que necesitan optimización específica. No tienes que elegir uno solo.

Para el acceso a datos en el contexto de ASP.NET Core puedes ver más en ASP.NET Core y el acceso a datos con EF, y para entender cómo encaja con el lenguaje, en C# y el stack de datos de Microsoft.

Imagen: Pexels / Daniil Komov

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP