EF Core 10 Performance Tips — 12 Ways to Make Your Queries Blazing Fast

EF Core 10 Performance 🔥 .NET 10 LTS

EF Core 10 Performance Tips — 12 Ways to Make Your Queries Blazing Fast

Stop letting Entity Framework Core slow down your .NET backend. These 12 real-world performance tips — with working C# code — will cut your query times, reduce memory usage, and make your database layer production-ready.

AS

Adnan Shaukat

May 19, 2026  ·  14 min read  ·  Backend Developer

Entity Framework Core is one of the most popular ORMs in the .NET ecosystem — and one of the most misused. I have reviewed dozens of ASP.NET Core backends where EF Core was the biggest performance bottleneck, not the application logic. Controllers loading thousands of tracked entities just to read them. N+1 query problems silently making 500 database calls per request. Bulk updates done row by row in a loop.

EF Core 10, running exclusively on the .NET 10 runtime, changes the performance story significantly. Benchmarks show 25–50% faster query execution just from upgrading to .NET 10 — before you change a single line of application code. Add the practical tips in this guide and you will have a database layer that performs at a professional level.

25–50%

faster queries on .NET 10 runtime

Zero

code changes needed for runtime gains

1 SQL

for bulk updates instead of thousands

LTS

supported until November 2028

Why EF Core 10 is Faster The .NET 10 Runtime Advantage

Before the tips — it is important to understand why EF Core 10 is faster. EF Core 10 runs exclusively on the .NET 10 runtime — it will not run on earlier versions. This compatibility line was drawn intentionally to unlock performance that was previously impossible.

The key runtime improvements that benefit EF Core directly include JIT compiler inlining that optimises escape analysis better, faster dictionary and span operations, a flattened pipeline that eliminates unpredictable branches in hot paths, and a faster ExpressionVisitor with caching of traversal results. Previously EF Core had to walk expression trees multiple times and allocate helper objects — now it does single-pass expression analysis with fewer allocations. The result is 25–50% faster data fetching benchmarks with the same application code.

Bottom line

Just upgrading your project to .NET 10 gives you a free 25–50% speed boost on EF Core queries. The tips below add even more on top of that baseline.

Tip 01 — Biggest Quick Win Use AsNoTracking for Read-Only Queries

By default EF Core tracks every entity it loads in the change tracker. This means it stores a snapshot of every row in memory so it can detect changes when you call SaveChanges(). For read-only queries — like loading data to display in an API response — this is completely wasted work.

C#
// ❌ Bad — tracking enabled, wastes memory and CPU var products = await context.Products .Where(p => p.IsActive) .ToListAsync(); // ✅ Good — no tracking, faster and less memory var products = await context.Products .AsNoTracking() .Where(p => p.IsActive) .ToListAsync(); // ✅ Even better — set it globally for the whole DbContext // Add this in your DbContext constructor: ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

Performance impact

AsNoTracking reduces memory usage significantly and can speed up read queries by 20–40% — especially when loading large result sets. Use it everywhere you are not planning to update the entities.

Tip 02 — Massive Impact Bulk Updates with ExecuteUpdateAsync and ExecuteDeleteAsync

This is the tip that makes the biggest difference in real production systems. The traditional EF Core pattern — load entities into memory, modify them, call SaveChanges — generates one SQL UPDATE statement per entity. For 10,000 rows that is 10,000 round trips to the database.

EF Core's ExecuteUpdateAsync and ExecuteDeleteAsync translate directly to a single SQL statement regardless of how many rows are affected.

C#
// ❌ Bad — loads ALL inactive users into memory then updates one by one var users = await context.Users .Where(u => u.LastLogin < DateTime.UtcNow.AddYears(-2)) .ToListAsync(); foreach (var user in users) user.IsActive = false; await context.SaveChangesAsync(); // 10,000 UPDATE statements! // ✅ Good — ONE SQL UPDATE statement for all rows await context.Users .Where(u => u.LastLogin < DateTime.UtcNow.AddYears(-2)) .ExecuteUpdateAsync(u => u.SetProperty(x => x.IsActive, false)); // ✅ Bulk delete — ONE SQL DELETE statement await context.Logs .Where(l => l.CreatedAt < DateTime.UtcNow.AddDays(-30)) .ExecuteDeleteAsync(); // ✅ Update multiple properties in one call await context.Products .Where(p => p.CategoryId == outdatedCategoryId) .ExecuteUpdateAsync(p => p .SetProperty(x => x.IsActive, false) .SetProperty(x => x.UpdatedAt, DateTime.UtcNow) .SetProperty(x => x.UpdatedBy, "system"));

Important note

ExecuteUpdateAsync and ExecuteDeleteAsync bypass the change tracker and do not fire EF Core events or interceptors. They also do not trigger SaveChanges. Use them for bulk operations on data you do not need to track individually.

Tip 03 — Hot Paths Compiled Queries for Frequently Called Endpoints

Every time EF Core executes a LINQ query, it goes through a compilation process — parsing the expression tree, translating it to SQL, and generating parameters. For a query that runs thousands of times per minute, this compilation overhead adds up. Compiled queries pre-compile the SQL translation once and reuse it on every call.

C#
// Define compiled queries as static fields — compiled ONCE at startup public static class CompiledQueries { // Compiled query — GetProductById public static readonly Func<AppDbContext, Guid, Task<Product?>> GetProductById = EF.CompileAsyncQuery((AppDbContext ctx, Guid id) => ctx.Products .AsNoTracking() .FirstOrDefault(p => p.Id == id && p.IsActive)); // Compiled query — GetActiveProductsByCategory public static readonly Func<AppDbContext, int, IAsyncEnumerable<Product>> GetByCategory = EF.CompileAsyncQuery((AppDbContext ctx, int categoryId) => ctx.Products .AsNoTracking() .Where(p => p.CategoryId == categoryId && p.IsActive) .OrderBy(p => p.Name)); } // Use it in your repository or service public async Task<Product?> GetByIdFastAsync(Guid id) { // No expression tree compilation on each call — reuses pre-compiled SQL return await CompiledQueries.GetProductById(_context, id); }

Tip 04 — Reduce Data Transfer Select Only What You Need — Use Projections

Loading a full entity when you only need two fields is one of the most common EF Core performance mistakes. Every column you load costs network bandwidth, memory allocation, and CPU time for object materialisation.

C#
// ❌ Bad — loads ALL 20 columns of Product just to show name and price var products = await context.Products .Where(p => p.IsActive) .ToListAsync(); // SELECT * FROM Products // ✅ Good — project to a DTO, load only needed columns var products = await context.Products .AsNoTracking() .Where(p => p.IsActive) .Select(p => new ProductSummaryDto(p.Id, p.Name, p.Price)) .ToListAsync(); // SELECT Id, Name, Price FROM Products // ✅ Anonymous type projection — great for one-off queries var summary = await context.Orders .AsNoTracking() .Where(o => o.CustomerId == customerId) .Select(o => new { o.Id, o.Total, o.CreatedAt, CustomerName = o.Customer.FullName }) .ToListAsync();

Tip 05 — Silent Performance Killer Fix the N+1 Query Problem

The N+1 problem is when your code executes 1 query to load a list, then N additional queries to load related data for each item. It is completely silent — your code looks clean but is destroying database performance.

C#
// ❌ VERY BAD — N+1 problem. 1 query for orders + 1 query PER order for customer var orders = await context.Orders.ToListAsync(); // 1 query foreach (var order in orders) { Console.WriteLine(order.Customer.Name); // Lazy load — 1 query per order! } // Result: 1 + 500 = 501 queries for 500 orders! // ✅ Fix 1 — Use Include() to eager load related data var orders = await context.Orders .AsNoTracking() .Include(o => o.Customer) // JOIN in single SQL query .Include(o => o.OrderItems) .ToListAsync(); // 1 query total // ✅ Fix 2 — Project to DTO (even better — no entity tracking) var orders = await context.Orders .AsNoTracking() .Select(o => new OrderDto( o.Id, o.Total, o.Customer.FullName, // EF Core handles the JOIN automatically o.OrderItems.Count )) .ToListAsync();

Tip 06 — Never Load All Rows Always Paginate Large Result Sets

C#
// ❌ Bad — loads entire table into memory var allProducts = await context.Products.ToListAsync(); // ✅ Good — cursor-based pagination (fastest for large tables) public async Task<List<ProductDto>> GetProductsPageAsync( Guid? lastId, int pageSize = 20) { var query = context.Products .AsNoTracking() .Where(p => p.IsActive) .OrderBy(p => p.Id); if (lastId.HasValue) query = query.Where(p => p.Id > lastId.Value) .OrderBy(p => p.Id); return await query .Take(pageSize) .Select(p => new ProductDto(p.Id, p.Name, p.Price)) .ToListAsync(); } // ✅ Traditional offset pagination (simple but slower on large tables) var page = await context.Products .AsNoTracking() .OrderBy(p => p.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .Select(p => new ProductDto(p.Id, p.Name, p.Price)) .ToListAsync();

Tip 07 — New in EF Core 10 LeftJoin and RightJoin — Finally First-Class LINQ Operators

For over a decade, writing a LEFT OUTER JOIN in LINQ required a cumbersome combination of GroupJoin, SelectMany, and DefaultIfEmpty. EF Core 10 adds LeftJoin and RightJoin as first-class LINQ operators that translate to proper SQL JOIN clauses.

C#
// ❌ Old way — verbose GroupJoin + SelectMany pattern (before EF Core 10) var results = context.Products .GroupJoin( context.Orders, p => p.Id, o => o.ProductId, (p, orders) => new { p, orders }) .SelectMany( x => x.orders.DefaultIfEmpty(), (x, o) => new { x.p.Name, OrderTotal = o != null ? o.Total : 0 }); // ✅ New EF Core 10 way — clean LeftJoin operator var results = context.Products .AsNoTracking() .LeftJoin( context.Orders, p => p.Id, o => o.ProductId, (p, o) => new { ProductName = p.Name, OrderTotal = o != null ? o.Total : (decimal?)0 }) .ToListAsync();

Tip 08 — New in EF Core 10 Native JSON Column Type — Faster JSON Storage and Querying

EF Core 10 fully supports the new native JSON data type in SQL Server 2025 and Azure SQL Database. Previously JSON was stored in nvarchar columns — string parsing was required for every query. The new JSON data type provides significant efficiency improvements and allows SQL Server to use native JSON indexing.

C#
// Entity with JSON-mapped complex types public class Blog { public int Id { get; set; } public string[] Tags { get; set; } // stored as native JSON array public required BlogDetails Details { get; set; } } public class BlogDetails { public string? Description { get; set; } public int Viewers { get; set; } } // Configure — EF Core 10 automatically uses native json type on SQL Server 2025 protected override void OnModelCreating(ModelBuilder builder) { builder.Entity<Blog>().OwnsOne( b => b.Details, d => d.ToJson()); // maps to native json column } // Query JSON properties with LINQ — uses JSON_VALUE() with RETURNING clause var popularBlogs = await context.Blogs .AsNoTracking() .Where(b => b.Details.Viewers > 1000) // native JSON index used! .OrderByDescending(b => b.Details.Viewers) .ToListAsync();

Tip 09 — Collection Includes Use Split Queries to Avoid Cartesian Explosion

When you use Include on multiple collection navigation properties, EF Core generates a JOIN that causes a cartesian explosion — the result set multiplies. An order with 5 items and 3 payments produces 15 rows instead of 8. For large collections this can cause massive data transfer.

C#
// ❌ Problem — cartesian explosion with multiple collection includes var orders = await context.Orders .Include(o => o.OrderItems) // 50 items per order .Include(o => o.Payments) // 3 payments per order .ToListAsync(); // returns 150 rows per order! // ✅ Fix — AsSplitQuery() runs separate SQL for each Include var orders = await context.Orders .AsNoTracking() .Include(o => o.OrderItems) .Include(o => o.Payments) .AsSplitQuery() // 3 clean SQL queries instead of 1 exploded JOIN .ToListAsync(); // ✅ Set split query globally for all queries in DbContext protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer(connectionString, o => o.UseQuerySplittingBehavior( QuerySplittingBehavior.SplitQuery));

Tip 10 — Complex Reports Use Raw SQL When LINQ Cannot Do It Efficiently

EF Core is not always the right tool. For complex reports, aggregations, or stored procedures, raw SQL is faster and gives you full control. EF Core 10 provides clean APIs for this that return typed results.

C#
// ✅ Raw SQL with typed return — safe parameterized query var report = await context.Database .SqlQuery<MonthlySalesDto>( $"EXEC GetMonthlySales {startDate}, {endDate}") .ToListAsync(); // ✅ FromSql for entities — safe, parameterized, composable with LINQ var products = await context.Products .FromSql($"SELECT * FROM Products WHERE CategoryId = {categoryId}") .AsNoTracking() .Where(p => p.IsActive) // compose additional LINQ on top! .OrderBy(p => p.Price) .ToListAsync(); // ✅ ExecuteSqlRawAsync for non-query commands (UPDATE/DELETE/stored proc) int rowsAffected = await context.Database .ExecuteSqlRawAsync( "EXEC UpdateInventory @p0, @p1", productId, newStock);

Tip 11 — Database Level Add Database Indexes — The Single Biggest Win

All the EF Core optimisations in the world will not help if your database tables have no indexes. A query against an unindexed column does a full table scan — reading every row in the table. An indexed query jumps directly to the matching rows. The difference can be from seconds to milliseconds.

C#
// Add indexes via Data Annotations [Index(nameof(Email), IsUnique = true)] [Index(nameof(IsActive), nameof(CreatedAt))] public class User { public Guid Id { get; set; } public string Email { get; set; } public bool IsActive { get; set; } public DateTime CreatedAt { get; set; } } // Or add via Fluent API in OnModelCreating — more control protected override void OnModelCreating(ModelBuilder builder) { builder.Entity<Product>(e => { // Unique index on product code e.HasIndex(p => p.Code).IsUnique(); // Composite index for frequent filter + sort combo e.HasIndex(p => new { p.CategoryId, p.IsActive, p.Price }); // Filtered index — only index active products e.HasIndex(p => p.Price) .HasFilter("[IsActive] = 1") .HasDatabaseName("IX_Products_Price_Active"); }); }

Index rule of thumb

Index every column you filter by in WHERE clauses, order by in ORDER BY, or join on. Always index foreign keys. Use composite indexes when you filter on multiple columns together regularly.

Tip 12 — AI Ready Vector Search — EF Core 10 for AI Workloads

EF Core 10 adds full support for the new vector data type in Azure SQL Database and SQL Server 2025. This allows you to store embeddings (numerical representations of meaning) and run similarity searches directly in the database — powering AI features like semantic search and RAG (Retrieval Augmented Generation).

C#
// Entity with vector embedding column public class BlogPost { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public float[] Embedding { get; set; } // stored as vector column } // Configure vector column protected override void OnModelCreating(ModelBuilder builder) { builder.Entity<BlogPost>() .Property(b => b.Embedding) .HasColumnType("vector(1536)"); // 1536 = OpenAI ada-002 dimensions } // Semantic similarity search — finds posts most similar to a query vector public async Task<List<BlogPost>> SearchSimilarAsync(float[] queryEmbedding) { return await context.BlogPosts .AsNoTracking() .OrderBy(b => EF.Functions.VectorDistance( "cosine", b.Embedding, queryEmbedding)) .Take(5) // top 5 most similar posts .ToListAsync(); }

Quick Reference EF Core 10 Performance Checklist

Use AsNoTracking() on all read-only queries

Replace update/delete loops with ExecuteUpdateAsync / ExecuteDeleteAsync

Use EF.CompileAsyncQuery for hot-path queries called thousands of times

Always project to DTOs using Select — never load full entities for reads

Fix N+1 queries with Include() or DTO projections

Always paginate — never load entire tables

Use AsSplitQuery() when including multiple collection navigations

Use LeftJoin/RightJoin operators instead of GroupJoin workarounds

Use native JSON columns on SQL Server 2025 / Azure SQL

Fall back to raw SQL for complex reports and stored procedures

Index every column used in WHERE, ORDER BY, and JOIN

Upgrade to .NET 10 runtime for free 25–50% baseline performance gain

Final Thoughts

EF Core 10 is the most performant version of Entity Framework ever shipped. Between the free .NET 10 runtime gains and the new query operators, you get better performance without rewriting a single business logic line. Apply the 12 tips in this guide to your project and your database layer will no longer be the bottleneck.

EF Core 10 Performance Entity Framework .NET 10 C# SQL Server Backend Database
AS

Written by

Adnan Shaukat

Backend Developer with 5+ years in .NET, Python, and REST APIs. Writing daily at codewithadnanshaukat.blogspot.com

Comments