Novedades de EF Core 9: las funciones que necesita conocer
Entity Framework Core 9 se lanzó junto con .NET 9 en noviembre de 2024 y, después de dedicar una gran cantidad de tiempo a trabajar con él en varios proyectos, puedo decir que es uno de los lanzamientos más significativos en mucho tiempo. No porque reinvente la rueda, sino porque pule las áreas donde EF Core históricamente ha causado la mayor fricción: traducción de consultas, rendimiento y trabajo con patrones de datos modernos.
En esta publicación, analizaré las funciones que han tenido el mayor impacto en mi trabajo diario. Si todavía estás en EF Core 8 (o incluso 7), esto debería darte una idea clara de lo que te espera al otro lado de la actualización.
EF Core 9 en el ecosistema .NET 9
EF Core 9 está dirigido a .NET 8 y .NET 9, lo que significa que no necesariamente necesita actualizar toda su aplicación a .NET 9 para aprovechar la mayoría de estas características. Dicho esto, algunas de las mejoras de rendimiento y AOT están estrechamente relacionadas con los cambios en el tiempo de ejecución de .NET 9, por lo que aprovechará al máximo si llega hasta el final.
El lanzamiento sigue la cadencia par/impar que Microsoft ha establecido: las versiones impares (como .NET 9) son soporte a plazo estándar (STS) con 18 meses de soporte, mientras que las versiones pares (como .NET 8) son soporte a largo plazo (LTS). Tenga esto en cuenta al planificar el cronograma de actualización.
Mejoras en la traducción de LINQ
Aquí es donde la mayoría de los desarrolladores sentirán la diferencia inmediatamente. EF Core 9 ha logrado avances significativos en la traducción de expresiones LINQ a SQL que realmente tienen sentido.
Mejores traducciones GroupBy
Si alguna vez escribió una consulta GroupBy en EF Core y terminó con advertencias de evaluación del lado del cliente o SQL extraño, conoce el problema. EF Core 9 maneja un conjunto mucho más amplio de GroupBy escenarios directamente en SQL.
var salesByCategory = await context.Products
.GroupBy(p => p.Category.Name)
.Select(g => new
{
Category = g.Key,
TotalRevenue = g.Sum(p => p.Price * p.UnitsSold),
AveragePrice = g.Average(p => p.Price),
ProductCount = g.Count()
})
.OrderByDescending(x => x.TotalRevenue)
.ToListAsync();
En versiones anteriores, las consultas que involucraban agregaciones sobre propiedades de navegación dentro de GroupBy a veces recurrían a la evaluación del cliente. EF Core 9 traduce esto claramente a una única consulta SQL con GROUP BY, SUM, AVG y COUNT.
Proyecciones y subconsultas complejas
Las subconsultas anidadas y las proyecciones complejas también obtuvieron una importante actualización. Considere algo como esto:
var orderSummaries = await context.Customers
.Select(c => new CustomerSummaryDto
{
Name = c.FullName,
TotalOrders = c.Orders.Count(),
MostRecentOrder = c.Orders
.OrderByDescending(o => o.OrderDate)
.Select(o => new OrderBriefDto
{
Id = o.Id,
Date = o.OrderDate,
Total = o.LineItems.Sum(li => li.Quantity * li.UnitPrice)
})
.FirstOrDefault(),
TopCategory = c.Orders
.SelectMany(o => o.LineItems)
.GroupBy(li => li.Product.Category.Name)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.FirstOrDefault()
})
.ToListAsync();
EF Core 9 ahora puede traducir esta expresión completa a SQL sin activar la evaluación del lado del cliente. La consulta generada utiliza subconsultas correlacionadas y uniones laterales cuando corresponde, y el plan SQL es considerablemente más eficiente que el que producirían versiones anteriores.
Colecciones primitivas parametrizadas
Una de las mejoras destacadas de LINQ es la capacidad de pasar colecciones de valores primitivos directamente a consultas:
var statusFilter = new List<string> { "Active", "Pending", "Review" };
var filteredOrders = await context.Orders
.Where(o => statusFilter.Contains(o.Status))
.ToListAsync();
En EF Core 8, esto se tradujo mediante cláusulas IN con valores insertados, lo que significaba que la caché del plan de consulta no se podía reutilizar cuando la lista cambiaba. EF Core 9 parametriza estas colecciones correctamente y las envía como un parámetro estructurado. Esto es muy importante para el almacenamiento en caché de planes de consultas en SQL Server y PostgreSQL.## Operaciones masivas: ejecutar actualización y ejecución de eliminación
ExecuteUpdate y ExecuteDelete se introdujeron en EF Core 7, pero EF Core 9 amplía lo que puedes hacer con ellos de maneras significativas.
Expresiones de actualización más complejas
Ahora puedes usar expresiones más complejas en ExecuteUpdate, incluidas referencias a otras tablas a través de propiedades de navegación:
await context.Products
.Where(p => p.Category.IsDiscontinued)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.IsAvailable, false)
.SetProperty(p => p.DiscontinuedDate, DateTimeOffset.UtcNow)
.SetProperty(p => p.Price, p => p.Price * 0.5m));
Esto genera una única declaración UPDATE con un JOIN en la tabla de categorías: no es necesario cargar entidades en la memoria, ni hay gastos generales de seguimiento de cambios.
Eliminaciones masivas condicionales con subconsultas
Las eliminaciones masivas con filtros de subconsultas ahora son totalmente compatibles:
await context.AuditLogs
.Where(log => log.CreatedAt < DateTime.UtcNow.AddYears(-2))
.Where(log => !context.ProtectedRecords
.Any(pr => pr.AuditLogId == log.Id))
.ExecuteDeleteAsync();
Esto se traduce en una DELETE con una subconsulta NOT EXISTS, exactamente lo que escribirías a mano. Sin entidades cargadas, sin viajes de ida y vuelta.
Mejoras en la columna JSON
Las columnas JSON han sido una de las características más interesantes de las versiones recientes de EF Core, y EF Core 9 las lleva más allá.
Consultando dentro de JSON
Ahora puede filtrar y proyectar datos desde columnas JSON con mejor soporte de traducción:
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public ShippingAddress Address { get; set; } // Stored as JSON
public List<OrderNote> Notes { get; set; } // Stored as JSON
}
public class ShippingAddress
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
}
public class OrderNote
{
public DateTime CreatedAt { get; set; }
public string Text { get; set; }
public string Author { get; set; }
}
Configuración en tu DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(builder =>
{
builder.OwnsOne(o => o.Address, ab =>
{
ab.ToJson();
});
builder.OwnsMany(o => o.Notes, nb =>
{
nb.ToJson();
});
});
}
Ahora puedes consultar directamente en JSON:
var newYorkOrders = await context.Orders
.Where(o => o.Address.State == "NY")
.OrderByDescending(o => o.Notes.Count)
.Select(o => new
{
o.CustomerName,
o.Address.City,
LatestNote = o.Notes
.OrderByDescending(n => n.CreatedAt)
.Select(n => n.Text)
.FirstOrDefault()
})
.ToListAsync();
EF Core 9 genera llamadas JSON_VALUE y JSON_QUERY adecuadas en SQL Server (o equivalente en otros proveedores) y la traducción cubre una gama mucho más amplia de operaciones LINQ en elementos JSON que antes.
Actualización de propiedades JSON
Uno de los puntos de fricción en EF Core 8 fue que la actualización de una sola propiedad dentro de una columna JSON provocaría que se reescribiera todo el documento JSON. EF Core 9 mejora esto con un seguimiento de cambios más granular para tipos asignados en JSON, generando actualizaciones más específicas cuando sea posible.
var order = await context.Orders.FindAsync(orderId);
order.Address.ZipCode = "10001";
await context.SaveChangesAsync();
En los proveedores compatibles, esto puede generar una modificación JSON más específica en lugar de reescribir todo el blob.
Tipos complejos: objetos de valor sin identidad
Los tipos complejos son una de las características que los profesionales del diseño basado en dominios estaban esperando. A diferencia de los tipos propios, los tipos complejos no tienen identidad: son objetos de valor puro.
[ComplexType]
public record Money(decimal Amount, string Currency);
[ComplexType]
public record DateRange(DateTime Start, DateTime End);
public class Project
{
public int Id { get; set; }
public string Name { get; set; }
public Money Budget { get; set; }
public DateRange Timeline { get; set; }
}
Estos se almacenan como columnas aplanadas en la tabla principal: Budget_Amount, Budget_Currency, Timeline_Start, Timeline_End, sin requerir una tabla separada ni ningún tipo de clave.
La diferencia clave con los tipos propios: los tipos complejos se comparan por valor, no por referencia. Dos instancias de Money con el mismo Amount y Currency se consideran iguales, independientemente de a qué entidad pertenezcan.
var expensiveProjects = await context.Projects
.Where(p => p.Budget.Amount > 100_000m && p.Budget.Currency == "USD")
.OrderByDescending(p => p.Budget.Amount)
.ToListAsync();
Esto se traduce directamente en filtrar las columnas aplanadas: limpio, eficiente y exactamente lo que esperarías.
Compatibilidad con HierarchyId para SQL Server
Si alguna vez ha trabajado con datos jerárquicos en SQL Server (organigramas, árboles de categorías, sistemas de archivos), sabrá que HierarchyId es el tipo integrado para esto. EF Core 9 le brinda soporte de primera clase.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Title { get; set; }
public HierarchyId PathFromCeo { get; set; }
}
Ahora puede consultar relaciones jerárquicas directamente:
var managerId = HierarchyId.Parse("/1/3/");
// All direct and indirect reports
var allReports = await context.Employees
.Where(e => e.PathFromCeo.IsDescendantOf(managerId))
.Where(e => e.PathFromCeo != managerId) // exclude the manager
.OrderBy(e => e.PathFromCeo)
.ToListAsync();
// Direct reports only (one level down)
var directReports = await context.Employees
.Where(e => e.PathFromCeo.GetAncestor(1) == managerId)
.ToListAsync();
// Get an employee's depth in the hierarchy
var employeesWithDepth = await context.Employees
.Select(e => new
{
e.Name,
e.Title,
Level = e.PathFromCeo.GetLevel()
})
.OrderBy(e => e.Level)
.ToListAsync();
```Todos estos se traducen a los métodos `HierarchyId` nativos de SQL Server. Si ha estado implementando estructuras de árbol con claves externas autorreferenciadas y CTE recursivas, este es un enfoque mucho más limpio.
## Modelos compilados y soporte AOT
Los desarrolladores preocupados por el rendimiento apreciarán la inversión continua en modelos compilados y soporte de compilación anticipada (AOT).
### Modelos compilados
Los modelos compilados generan previamente los metadatos del modelo que EF Core normalmente crea al inicio. Para modelos grandes (piense en cientos de entidades), esto puede reducir drásticamente el tiempo de arranque en frío.
```bash
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels
Luego conéctalo:
builder.Services.AddDbContext<AppDbContext>(options =>
options
.UseSqlServer(connectionString)
.UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));
En EF Core 9, los modelos compilados son más completos: admiten más funciones de mapeo y generan resultados más pequeños. Para un modelo con alrededor de 400 entidades, el tiempo de inicio puede disminuir desde varios segundos hasta casi instantáneo.
Progreso de la compilación de AOT
La compatibilidad total con AOT nativo para EF Core aún es un trabajo en progreso, pero EF Core 9 logra avances significativos. Muchas de las rutas de código con mucha reflexión se han refactorizado para que sean fáciles de recortar, y los modelos compilados son una parte clave de la historia de AOT. Si su objetivo son escenarios como Azure Functions o microservicios donde el arranque en frío es importante, estas mejoras son directamente relevantes.
Actualizaciones del proveedor de Cosmos DB
El proveedor Azure Cosmos DB continúa madurando con EF Core 9. Algunas mejoras notables:
Manejo de claves de partición
El proveedor ahora admite claves de partición jerárquicas y maneja los filtros de claves de partición de manera más inteligente:
public class TenantDocument
{
public string Id { get; set; }
public string TenantId { get; set; } // Partition key
public string Region { get; set; } // Sub-partition key
public string Content { get; set; }
}
// This query now correctly uses the partition key for routing
var docs = await context.TenantDocuments
.Where(d => d.TenantId == "tenant-42" && d.Region == "us-east")
.Where(d => d.Content.Contains("important"))
.ToListAsync();
Traducción mejorada de LINQ a NoSQL
Más operaciones LINQ ahora se traducen al dialecto SQL de Cosmos DB, incluida una mejor compatibilidad con Contains, Any, operaciones de matrices anidadas y funciones matemáticas. Las consultas que antes recaían en la evaluación del cliente ahora se manejan en el lado del servidor.
Soporte de búsqueda vectorial
EF Core 9 presenta soporte temprano para la búsqueda de similitudes vectoriales con Cosmos DB, lo cual es útil si está creando aplicaciones que se integran con incrustaciones o búsqueda basada en IA:
var results = await context.Documents
.OrderBy(d => EF.Functions.VectorDistance(d.Embedding, queryVector))
.Take(10)
.ToListAsync();
Mejoras en la migración
Las migraciones obtuvieron algunas mejoras en la calidad de vida que hacen que trabajar con ellas sea menos doloroso en entornos de equipo.
Tablas Temporales en Migraciones
Las migraciones ahora manejan la configuración de la tabla temporal de manera más elegante, con soporte adecuado para columnas de período y nombres de tablas de historial:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.ToTable("Employees", b => b.IsTemporal(t =>
{
t.HasPeriodStart("ValidFrom");
t.HasPeriodEnd("ValidTo");
t.UseHistoryTable("EmployeeHistory");
}));
}
Scripts idempotentes
El comando Script-Migration (y su equivalente CLI) produce mejores scripts idempotentes de forma predeterminada, con un manejo mejorado de casos extremos en torno a cambios de esquema que dependen de los datos existentes en ciertos estados.
Paquetes de migración
Los paquetes de migración, que empaquetan sus migraciones en un ejecutable independiente para su implementación, son más confiables en EF Core 9 con mejores informes de errores y lógica de reintento para fallas transitorias.
dotnet ef migrations bundle --self-contained -r linux-x64
Esto produce un binario que puede ejecutar en su canal de CI/CD sin necesidad de que el SDK de .NET esté instalado en su destino de implementación.
Puntos de referencia de rendimientoAquí hay algunos puntos de referencia aproximados de mis propias pruebas. Estos pertenecen a un proyecto con aproximadamente 200 entidades, que se ejecuta en SQL Server 2022, medido con BenchmarkDotNet. Sus números variarán, pero las mejoras relativas deberían ser similares.
| Escenario | EF Núcleo 8 | EF Núcleo 9 | Mejora |
|---|---|---|---|
| Construcción del modelo (arranque en frío) | 1.850 ms | 320 ms | ~5,8 veces más rápido (compilado) |
| Consulta simple (entidad única por PK) | 0,42 ms | 0,38 ms | ~10% más rápido |
| Consulta compleja (uniones + agregación) | 3,1 ms | 2,4 ms | ~23% más rápido |
| Actualización masiva (10.000 filas) | 145 ms | 118ms | ~19% más rápido |
| Consulta de columna JSON | 2,8 ms | 1,9 ms | ~32% más rápido |
| SaveChanges (100 entidades) | 48ms | 41ms | ~15% más rápido |
La mejora del modelo compilado es la más espectacular, pero las mejoras constantes en todos los ámbitos se suman, especialmente en escenarios de alto rendimiento en los que se ejecutan miles de consultas por segundo.
Actualización desde EF Core 8
Si está en EF Core 8, la ruta de actualización es relativamente sencilla. Aquí hay una lista de verificación:
1. Actualiza tus paquetes:
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
2. Compruebe si hay cambios importantes. La lista de EF Core 9 es relativamente corta en comparación con algunas versiones anteriores. Los más notables:
- Se han eliminado algunas API previamente obsoletas.
- Cambios en cómo se traducen ciertas consultas
GroupBy(ahora van al lado del servidor, lo que cambia el comportamiento si dependiera de la evaluación del cliente) - Cambios menores en la producción del andamiaje de migración.
3. Vuelva a generar modelos compilados si los está utilizando. El formato cambió, por lo que los modelos compilados antiguos no funcionarán con EF Core 9.
4. Ejecute su conjunto de pruebas. Preste especial atención a las consultas que se evaluaron previamente en el cliente; es posible que ahora se evalúen en el servidor, lo que suele ser mejor, pero podría revelar diferencias en los datos.
5. Verifique sus consultas de Cosmos DB si está utilizando ese proveedor. Las traducciones mejoradas significan que algunas consultas se ejecutarán de manera diferente (generalmente más rápido), pero vale la pena verificar que los resultados sean idénticos.
Una actualización mínima para un proyecto típico se ve así:
dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet build
dotnet test
Si todo se compila y las pruebas pasan, probablemente esté en buena forma. Si tiene problemas, la documentación de cambios importantes de EF Core 9 tiene una guía de migración detallada para cada cambio.
Concluyendo
EF Core 9 no es una versión revolucionaria: es evolutiva y eso es exactamente lo que tenía que ser. Las mejoras de LINQ por sí solas justifican la actualización para la mayoría de los proyectos, y características como mejoras en las columnas JSON, tipos complejos y HierarchyId admiten patrones abiertos que antes eran incómodos o imposibles.
Si tuviera que elegir las tres características que han tenido mayor impacto en mis proyectos:
- Colecciones primitivas parametrizadas: porque la eficiencia de la caché del plan de consultas es importante a escala
- Mejoras en las columnas JSON: porque el patrón híbrido de documento relacional es increíblemente útil
- Modelos compilados: porque el tiempo de inicio afecta directamente la productividad del desarrollador y la velocidad de implementación.El equipo de EF Core ha seguido una trayectoria sólida desde EF Core 5 y la versión 9 continúa esa tendencia. Si ya tiene EF Core 8, la actualización es de bajo riesgo y alta recompensa. Si estás en algo más antiguo, nunca ha habido un mejor momento para dar el salto.
Codificación feliz y consultas felices.