Entity Framework  

EF Core 11 Performance Tips Every Developer Should Know

Entity Framework Core (EF Core) is one of the most popular Object-Relational Mappers (ORMs) for .NET applications. It simplifies data access, reduces boilerplate code, and integrates seamlessly with ASP.NET Core. However, inefficient EF Core usage can lead to slow queries, excessive memory consumption, and unnecessary database round trips.

Many performance problems are not caused by EF Core itself but by how it is used. Simple changes such as disabling tracking for read-only queries, selecting only required columns, or avoiding the N+1 query problem can significantly improve application performance.

In this article, you'll learn practical EF Core 11 performance optimization techniques, understand when to apply them, and learn how to evaluate their impact using a structured benchmarking methodology.

Note: This article focuses on optimization techniques and benchmark methodology. It does not present fabricated benchmark numbers.

Why EF Core Performance Matters

Every database operation consumes resources.

Poorly optimized queries can result in:

  • Higher database CPU usage

  • Increased memory allocations

  • Longer response times

  • More network traffic

  • Lower application scalability

Optimizing data access is often one of the most effective ways to improve application performance.

Sample Entity

We'll use the following model throughout the examples.

public class Product
{
    public int Id { get; set; }

    public string Name { get; set; } = "";

    public decimal Price { get; set; }

    public int CategoryId { get; set; }

    public Category Category { get; set; } = null!;
}

Use AsNoTracking for Read-Only Queries

By default, EF Core tracks every entity it loads.

var products = await context.Products
    .ToListAsync();

If the data will not be modified, disable change tracking.

var products = await context.Products
    .AsNoTracking()
    .ToListAsync();

Why It Helps

AsNoTracking():

  • Reduces memory usage

  • Lowers CPU overhead

  • Improves query performance

  • Is ideal for APIs and reporting

Use tracking only when updates are required.

Select Only Required Columns

Avoid retrieving entire entities when only a few fields are needed.

Instead of:

var products = await context.Products
    .ToListAsync();

Project the required columns.

var products = await context.Products
    .Select(p => new
    {
        p.Id,
        p.Name,
        p.Price
    })
    .ToListAsync();

Smaller result sets reduce network traffic and memory consumption.

Avoid the N+1 Query Problem

Consider the following code.

var products = await context.Products
    .ToListAsync();

foreach (var product in products)
{
    Console.WriteLine(product.Category.Name);
}

This may execute one query for products and additional queries for each category.

Use eager loading instead.

var products = await context.Products
    .Include(p => p.Category)
    .ToListAsync();

This retrieves related data in a single query.

Use Filtered Includes

If only part of a navigation property is required, filter it.

var categories = await context.Categories
    .Include(c => c.Products
        .Where(p => p.Price > 100))
    .ToListAsync();

Filtered includes reduce unnecessary data retrieval.

Use Pagination

Loading thousands of rows into memory is rarely necessary.

Instead of:

var products = await context.Products
    .ToListAsync();

Use pagination.

var products = await context.Products
    .OrderBy(p => p.Id)
    .Skip(0)
    .Take(20)
    .ToListAsync();

Pagination improves response times and reduces memory usage.

Use Compiled Queries

Frequently executed queries can be compiled.

private static readonly Func<AppDbContext, int, Task<Product?>>
GetProduct =
    EF.CompileAsyncQuery(
        (AppDbContext db, int id) =>
            db.Products.FirstOrDefault(p => p.Id == id));

Execute the compiled query.

var product = await GetProduct(context, 10);

Compiled queries reduce query compilation overhead for frequently executed operations.

Execute Bulk Updates

Instead of loading entities into memory before updating them:

var products = await context.Products.ToListAsync();

foreach (var product in products)
{
    product.Price += 10;
}

await context.SaveChangesAsync();

Use ExecuteUpdateAsync().

await context.Products
    .ExecuteUpdateAsync(p =>
        p.SetProperty(
            x => x.Price,
            x => x.Price + 10));

This performs the update directly in the database.

Execute Bulk Deletes

Similarly, avoid loading rows before deleting them.

await context.Products
    .Where(p => p.Price == 0)
    .ExecuteDeleteAsync();

This generates a direct SQL DELETE statement.

Add Database Indexes

Frequently filtered columns should be indexed.

modelBuilder.Entity<Product>()
    .HasIndex(p => p.CategoryId);

Indexes can significantly improve query performance for search and join operations.

Monitor Generated SQL

Inspect the SQL generated by EF Core.

var query = context.Products
    .Where(p => p.Price > 500);

Console.WriteLine(query.ToQueryString());

Reviewing generated SQL helps identify unnecessary joins, filters, and inefficient queries.

End-to-End Query Flow

A typical optimized request follows these steps:

  1. API receives a request.

  2. EF Core generates SQL.

  3. Database executes the query.

  4. Only required columns are returned.

  5. Change tracking is skipped for read-only data.

  6. Results are serialized and returned.

Optimizing each stage improves overall application performance.

Optimization Comparison

TechniquePerformance BenefitBest For
AsNoTrackingLower CPU and memoryRead-only queries
ProjectionReduced network trafficAPIs
IncludeEliminates N+1 queriesRelated data
Filtered IncludeSmaller result setsLarge relationships
PaginationLower memory usageLarge tables
Compiled QueriesLower query compilation overheadFrequently executed queries
ExecuteUpdateAsyncFaster bulk updatesBatch operations
ExecuteDeleteAsyncFaster bulk deletesCleanup tasks

Performance Evaluation Methodology

The research brief emphasizes performance but does not provide benchmark data. Use the following methodology to evaluate optimizations.

Test Environment

Keep these variables consistent:

  • .NET SDK version

  • EF Core version

  • Database engine

  • Hardware

  • Dataset size

  • Build configuration

Test Scenarios

Compare:

  • Tracking vs No Tracking

  • Full entity vs Projection

  • Include vs Lazy Loading

  • Regular queries vs Compiled Queries

  • Traditional updates vs ExecuteUpdateAsync

  • Traditional deletes vs ExecuteDeleteAsync

Metrics to Measure

Collect:

  • Query execution time

  • Memory allocations

  • Logical reads

  • Database CPU usage

  • Network traffic

  • Rows returned

Useful Tools

Use:

  • BenchmarkDotNet

  • SQL Server Query Store

  • dotnet-counters

  • dotnet-trace

  • SQL Server Execution Plans

  • ToQueryString()

Always benchmark with production-like datasets instead of small development databases.

Best Practices

  • Use AsNoTracking() for read-only operations.

  • Project only required columns.

  • Avoid the N+1 query problem.

  • Apply pagination for large datasets.

  • Use compiled queries for frequently executed operations.

  • Create indexes for commonly filtered columns.

  • Prefer ExecuteUpdateAsync() and ExecuteDeleteAsync() for bulk operations.

  • Review generated SQL during development.

Common Mistakes

MistakeImpact
Returning entire entities unnecessarilyIncreased memory and network usage
Loading all records into memoryPoor scalability
Ignoring indexesSlow queries
Excessive lazy loadingN+1 query problem
Tracking read-only entitiesHigher CPU and memory usage
Not reviewing generated SQLHidden performance issues

Troubleshooting

Queries Are Slow

Check:

  • Missing indexes

  • Execution plans

  • Query projections

  • Tracking configuration

  • Database statistics

High Memory Usage

Review:

  • Entity tracking

  • Result set size

  • Pagination

  • Projection

Too Many SQL Queries

Investigate:

  • Lazy loading

  • Navigation property access

  • Missing Include() statements

Use logging and SQL profiling to identify unexpected database calls.

FAQs

Should I always use AsNoTracking()?

No. Use it for read-only queries. If you plan to update entities, tracking is required.

When should I use compiled queries?

Compiled queries are beneficial for queries executed frequently with the same structure, especially in high-throughput applications.

Is projection faster than loading full entities?

In many cases, yes. Retrieving only the required columns reduces network traffic, memory usage, and serialization overhead.

Are ExecuteUpdateAsync() and ExecuteDeleteAsync() better than SaveChanges()?

For bulk operations, yes. They execute directly in the database without loading entities into memory.

How do I identify EF Core performance issues?

Review generated SQL, analyze execution plans, monitor database metrics, and benchmark representative workloads using tools such as BenchmarkDotNet and Query Store.

Conclusion

EF Core provides excellent productivity, but achieving high performance requires thoughtful query design and efficient data access patterns. Techniques such as disabling tracking for read-only queries, projecting only required columns, avoiding N+1 queries, using compiled queries, and leveraging bulk operations can significantly improve scalability and responsiveness.

Rather than applying every optimization indiscriminately, measure each change using realistic workloads and production-like datasets. A data-driven approach ensures your EF Core applications remain both maintainable and performant as they grow.