# Entity Framework Core: Optimización del Rendimiento y Buenas Prácticas en 2026 > Guía completa de optimización de rendimiento con Entity Framework Core 10 en .NET 10. AsNoTracking, consultas compiladas, actualizaciones por lotes, split queries y LeftJoin. - Published: 2026-04-15 - Updated: 2026-04-15 - Author: SharpSkill - Tags: dotnet, entity-framework-core, performance, best-practices - Reading time: 10 min --- Entity Framework Core 10, distribuido con .NET 10 LTS en noviembre de 2025, introduce operadores LeftJoin, búsqueda vectorial, filtros de consulta con nombre y mejoras significativas en la traducción SQL. Esta guía cubre las buenas prácticas de EF Core que impactan directamente la velocidad de las consultas, la asignación de memoria y la escalabilidad en producción. > **EF Core 10 solo funciona con .NET 10** > > EF Core 10 es una versión Long-Term Support, con soporte hasta noviembre de 2028. Requiere el SDK y el runtime de .NET 10. Las aplicaciones que todavía utilizan .NET 8 deben apuntar a EF Core 8 (LTS) hasta completar la migración. ## Seguimiento de Consultas: Cuándo Desactivarlo y Por Qué Cada llamada a `DbSet` adjunta las entidades devueltas al change tracker de forma predeterminada. El tracker mantiene una instantánea de los valores originales de las propiedades, calcula las diferencias al ejecutar `SaveChanges` y resuelve conflictos de identidad entre las navegaciones. Esa sobrecarga es innecesaria cuando los datos fluyen directamente hacia una respuesta de API o un modelo de vista de solo lectura. ```csharp // ProductRepository.cs public async Task> GetActiveByCategoryAsync( int categoryId, CancellationToken ct) { return await _context.Products .AsNoTracking() // omitir el change tracker por completo .Where(p => p.CategoryId == categoryId && p.IsActive) .OrderBy(p => p.Name) .Select(p => new ProductDto // proyectar al DTO a nivel de base de datos { Id = p.Id, Name = p.Name, Price = p.Price }) .ToListAsync(ct); } ``` `AsNoTracking` elimina la sobrecarga por entidad de la creación de instantáneas y la resolución de identidad. Combinado con `Select`, solo las columnas necesarias viajan por la red. En una tabla con 50,000 registros, este patrón reduce típicamente las asignaciones de memoria entre un 40 y un 60% en comparación con una consulta completa con seguimiento. Para contextos que nunca modifican datos, se puede establecer el comportamiento predeterminado durante el registro: ```csharp // Program.cs builder.Services.AddDbContext(options => options.UseSqlServer(connectionString) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); ``` ## Split Queries para Evitar la Explosión Cartesiana Cargar una entidad con múltiples navegaciones de colección mediante `Include` genera una única sentencia SQL con JOINs. Cuando se cargan dos o más colecciones simultáneamente, el conjunto de resultados crece como un producto cartesiano de las colecciones, duplicando los datos de la fila padre en cada combinación. ```csharp // OrderRepository.cs public async Task GetWithDetailsAsync(int orderId, CancellationToken ct) { return await _context.Orders .AsSplitQuery() // un SQL por cada Include .Include(o => o.Items) .ThenInclude(i => i.Product) .Include(o => o.Payments) .Include(o => o.ShippingEvents) .FirstOrDefaultAsync(o => o.Id == orderId, ct); } ``` `AsSplitQuery` descompone la carga en sentencias SQL separadas por cada navegación. La contrapartida: múltiples viajes de ida y vuelta en lugar de uno, pero cada conjunto de resultados permanece pequeño y evita el problema de duplicación de filas. EF Core 10 también corrige una inconsistencia de larga data en el ordenamiento de las split queries, asegurando que el orden de la subconsulta coincida con el de la consulta principal. > **Cuándo usar consultas únicas** > > Las consultas únicas siguen siendo preferibles al cargar una sola navegación de colección o cuando la latencia de los viajes de ida y vuelta es alta (llamadas a bases de datos entre regiones). Es recomendable hacer benchmark de ambos modos para el patrón de acceso específico antes de tomar una decisión. ## Operaciones por Lotes con ExecuteUpdate y ExecuteDelete El flujo de trabajo tradicional de EF carga las entidades, modifica las propiedades y luego llama a `SaveChanges`. Para operaciones masivas que afectan miles de filas, esto crea miles de instancias rastreadas y sentencias UPDATE individuales. EF Core 7 introdujo `ExecuteUpdateAsync` y `ExecuteDeleteAsync` para enviar la operación como un único comando SQL. EF Core 10 simplifica aún más la API al aceptar una lambda simple en lugar de un árbol de expresiones. ```csharp // PromotionService.cs public async Task ApplySeasonalDiscountAsync( int categoryId, decimal discountPercent, CancellationToken ct) { var affected = await _context.Products .Where(p => p.CategoryId == categoryId && p.IsActive) .ExecuteUpdateAsync(s => { s.SetProperty(p => p.Price, p => p.Price * (1 - discountPercent / 100)); s.SetProperty(p => p.LastModified, DateTime.UtcNow); }, ct); // affected = número de filas actualizadas } ``` Esto se traduce en una única sentencia `UPDATE ... SET ... WHERE`. No se cargan entidades en memoria. Para una actualización de 10,000 filas, el tiempo de ejecución pasa de varios segundos (enfoque con seguimiento) a milisegundos. El mismo patrón aplica para las eliminaciones: ```csharp // CleanupService.cs public async Task PurgeExpiredSessionsAsync(CancellationToken ct) { await _context.Sessions .Where(s => s.ExpiresAt < DateTime.UtcNow) .ExecuteDeleteAsync(ct); } ``` ## El Operador LeftJoin en EF Core 10 Antes de EF Core 10, realizar un LEFT JOIN requería combinar `GroupJoin`, `SelectMany` y `DefaultIfEmpty` en un patrón específico que la mayoría de los desarrolladores tenían que buscar cada vez. EF Core 10 agrega `LeftJoin` como un operador LINQ de primera clase. ```csharp // ReportService.cs public async Task> GetEmployeeDepartmentReportAsync( CancellationToken ct) { return await _context.Employees .LeftJoin( _context.Departments, employee => employee.DepartmentId, department => department.Id, (employee, department) => new EmployeeReportDto { FullName = employee.FirstName + " " + employee.LastName, Department = department.Name ?? "Unassigned", HiredAt = employee.HiredAt }) .OrderBy(r => r.FullName) .ToListAsync(ct); } ``` El SQL generado utiliza una cláusula `LEFT JOIN` estándar. El operador `RightJoin` también está disponible. Ambos operadores eliminan la cadena verbosa de tres métodos que se requería anteriormente. ## Filtros de Consulta con Nombre para Filtrado de Múltiples Preocupaciones Los filtros de consulta globales han estado limitados a un solo filtro por tipo de entidad desde EF Core 2.0. Las aplicaciones que implementan tanto eliminación lógica como multi-tenancy tenían que combinar las condiciones en una sola expresión y no podían desactivarlas selectivamente. EF Core 10 introduce los filtros de consulta con nombre. ```csharp // AppDbContext.cs protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasQueryFilter("SoftDelete", i => !i.IsDeleted) .HasQueryFilter("Tenant", i => i.TenantId == _tenantId); } ``` Los endpoints de administración pueden desactivar el filtro de eliminación lógica mientras mantienen activo el aislamiento de tenants: ```csharp // InvoiceRepository.cs public async Task> GetAllIncludingDeletedAsync(CancellationToken ct) { return await _context.Invoices .IgnoreQueryFilters(["SoftDelete"]) // el filtro de tenant sigue aplicado .ToListAsync(ct); } ``` Esto elimina la necesidad de métodos de extensión `IQueryable` personalizados que agregaban manualmente las condiciones de filtrado. ## Resiliencia de Conexiones y Configuración del Pool Las fallas transitorias de base de datos (interrupciones de red, failovers de Azure SQL, agotamiento del pool de conexiones) causan excepciones que frecuentemente hacen fallar los pipelines de solicitudes. EF Core proporciona lógica de reintento integrada, pero los valores predeterminados requieren configuración explícita. ```csharp // Program.cs builder.Services.AddDbContext(options => options.UseSqlServer(connectionString, sqlOptions => { sqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(10), errorNumbersToAdd: null); // reintentar en todos los errores transitorios sqlOptions.CommandTimeout(30); // timeout de comando de 30 segundos })); ``` Los parámetros de pooling de la cadena de conexión también importan: ``` Server=db.example.com;Database=AppDb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=15; ``` `Min Pool Size` mantiene conexiones preparadas para ráfagas de tráfico. `Max Pool Size` limita el total de conexiones abiertas para prevenir la sobrecarga de la base de datos. El valor predeterminado de 100 funciona para la mayoría de las aplicaciones web, pero los servicios de alto rendimiento pueden necesitar ajustes basados en el volumen real de consultas concurrentes. > **Lógica de reintento y transacciones** > > Los reintentos automáticos no funcionan dentro de transacciones iniciadas por el usuario. Es necesario envolver toda la transacción en una estrategia de reintento manual usando `context.Database.CreateExecutionStrategy().ExecuteAsync(...)` para manejar fallas transitorias a través de múltiples operaciones. ## Estrategia de Indexación y Análisis de Consultas Las migraciones de EF Core permiten definir índices de forma declarativa, pero elegir qué columnas indexar requiere comprender los patrones de consulta. ```csharp // AppDbContext.cs - OnModelCreating modelBuilder.Entity(entity => { // índice compuesto para un patrón de consulta frecuente entity.HasIndex(o => new { o.CustomerId, o.Status, o.CreatedAt }) .HasDatabaseName("IX_Order_Customer_Status_Date"); // índice filtrado solo para pedidos activos entity.HasIndex(o => o.Status) .HasFilter("[Status] <> 'Cancelled'") .HasDatabaseName("IX_Order_ActiveStatus"); }); ``` Los índices filtrados reducen el tamaño del índice al excluir filas que las consultas nunca apuntan. Para una tabla con un 80% de pedidos cancelados, un índice filtrado sobre estados activos puede ser 5 veces más pequeño y rápido de recorrer. Para analizar el SQL generado, se puede habilitar el registro en desarrollo: ```csharp // Program.cs (solo desarrollo) builder.Services.AddDbContext(options => options.UseSqlServer(connectionString) .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging()); ``` Se recomienda copiar el SQL registrado en SQL Server Management Studio y ejecutarlo con `SET STATISTICS IO ON` para verificar las lecturas lógicas, o usar `EXPLAIN ANALYZE` en PostgreSQL. Las sugerencias de índices faltantes del plan de consulta suelen revelar las oportunidades de optimización de mayor impacto. ## Consultas Compiladas para Rutas Críticas Los árboles de expresiones LINQ se analizan y traducen a SQL en cada ejecución. Para consultas que se ejecutan miles de veces por minuto (búsquedas de autenticación, validación de sesiones), este costo de traducción se acumula. Las consultas compiladas almacenan en caché la traducción al iniciar la aplicación. ```csharp // CompiledQueries.cs public static class UserQueries { // compilada una vez, reutilizada en cada llamada public static readonly Func> GetActiveSession = EF.CompileAsyncQuery( (AppDbContext ctx, string token, CancellationToken ct) => ctx.UserSessions .AsNoTracking() .FirstOrDefault(s => s.Token == token && s.ExpiresAt > DateTime.UtcNow)); } // Uso en middleware var session = await UserQueries.GetActiveSession(dbContext, bearerToken, ct); ``` Las consultas compiladas omiten por completo la fase de análisis del árbol de expresiones. La diferencia de rendimiento solo es medible en rutas de alta frecuencia (más de 1,000 llamadas/minuto). Para endpoints CRUD estándar, la caché de consultas integrada de EF Core ya maneja la reutilización de la traducción. ## Traducción de Colecciones Parametrizadas en EF Core 10 Las consultas que filtran por una lista de IDs representan uno de los patrones más comunes en el acceso a datos. EF Core 10 cambia la estrategia de traducción predeterminada para colecciones parametrizadas. En lugar de codificar la lista como un arreglo JSON (EF Core 8-9) o insertar constantes en línea (EF Core 7 y anteriores), EF Core 10 traduce cada valor como un parámetro SQL separado. ```csharp // Antes (EF Core 8-9): parámetro de arreglo JSON // @__ids_0='[1,2,3]' // SELECT ... WHERE Id IN (SELECT value FROM OPENJSON(@__ids_0)) // EF Core 10: parámetros individuales con padding // SELECT ... WHERE Id IN (@ids1, @ids2, @ids3) var orderIds = new[] { 101, 205, 389 }; var orders = await _context.Orders .Where(o => orderIds.Contains(o.Id)) .ToListAsync(ct); ``` El nuevo enfoque proporciona al planificador de consultas de la base de datos información de cardinalidad mientras evita la saturación de la caché de planes mediante el padding de parámetros. Para casos donde el enfoque JSON tiene mejor rendimiento (colecciones muy grandes), se puede sobrescribir el comportamiento por consulta: ```csharp var orders = await _context.Orders .Where(o => EF.Parameter(orderIds).Contains(o.Id)) // forzar modo JSON .ToListAsync(ct); ``` ## Conclusión - Usar `AsNoTracking()` y proyecciones `Select` en cada consulta de solo lectura para eliminar la sobrecarga del change tracker - Aplicar `AsSplitQuery()` al cargar múltiples navegaciones de colección para evitar la explosión cartesiana - Reemplazar los patrones de cargar-modificar-guardar con seguimiento por `ExecuteUpdateAsync` y `ExecuteDeleteAsync` para operaciones masivas - Adoptar el operador `LeftJoin` de EF Core 10 para reemplazar la cadena verbosa `GroupJoin`/`SelectMany`/`DefaultIfEmpty` - Configurar los filtros de consulta con nombre para gestionar eliminación lógica y multi-tenancy de forma independiente - Configurar `EnableRetryOnFailure` y ajustar los tamaños del pool de conexiones para la resiliencia en producción - Definir índices compuestos y filtrados basados en patrones de consulta reales, no en suposiciones - Reservar las consultas compiladas para rutas genuinamente críticas que superen las 1,000 ejecuciones por minuto - Dejar que EF Core 10 maneje la traducción de colecciones parametrizadas por defecto, y sobrescribir solo cuando los benchmarks lo justifiquen Para profundizar: el [módulo avanzado de EF Core](/technologies/dotnet/interview-questions/ef-core-advanced) cubre estos patrones en un contexto de entrevista técnica, y la [guía de Clean Architecture con .NET](/blog/dotnet/clean-architecture-dotnet-practical-guide) demuestra cómo estructurar las capas de repositorio y servicio que encapsulan estas consultas. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/dotnet/ef-core-performance-best-practices