DEV Community

Cover image for 10 Entity Framework Core Performance Tips Every .NET Developer Should Know
ToolBench
ToolBench

Posted on

10 Entity Framework Core Performance Tips Every .NET Developer Should Know

Entity Framework Core makes it incredibly easy to work with databases in .NET applications.

You can write C# instead of SQL, work with strongly typed entities, use LINQ for queries, and let EF Core handle much of the database interaction for you.

But there's an important catch:

Writing code that works is not the same as writing code that performs well.

A LINQ query can look perfectly clean in C# while generating an inefficient SQL query behind the scenes. As your database grows from thousands to millions of records, small mistakes can turn into serious performance problems.

In this article, we'll look at 10 practical Entity Framework Core performance techniques that can make a real difference in ASP.NET Core applications.

We'll cover:

  • How to avoid unnecessary columns
  • Why AsNoTracking() matters
  • Pagination
  • Avoiding N+1 queries
  • Eager loading
  • Projection with Select()
  • Database indexes
  • Avoiding unnecessary ToList()
  • Query compilation and caching
  • How to inspect the SQL generated by EF Core

Let's get started.


📖 Table of Contents

  1. Understand What EF Core Is Actually Doing
  2. Select Only the Columns You Need
  3. Use AsNoTracking() for Read-Only Queries
  4. Always Consider Pagination
  5. Avoid the N+1 Query Problem
  6. Be Careful With Include()
  7. Prefer Projection with Select()
  8. Use Database Indexes
  9. Don't Call ToList() Too Early
  10. Inspect the SQL Generated by EF Core
  11. Combine Techniques for Real Applications
  12. Common EF Core Performance Mistakes
  13. Practical Checklist
  14. EF Core Interview Questions
  15. Frequently Asked Questions
  16. Final Thoughts

1. Understand What EF Core Is Actually Doing

Before optimizing EF Core, you need to understand one fundamental concept.

When you write:

var employees = await context.Employees
    .Where(e => e.Department == "IT")
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

EF Core doesn't simply execute C# code against the database.

Instead, EF Core translates the LINQ expression into SQL.

Conceptually:

C# LINQ
   ↓
EF Core
   ↓
SQL Query
   ↓
Database
   ↓
Results
   ↓
EF Core
   ↓
C# Objects
Enter fullscreen mode Exit fullscreen mode

Your C# code might look simple:

context.Employees
    .Where(e => e.Department == "IT")
Enter fullscreen mode Exit fullscreen mode

But the database ultimately needs something similar to:

SELECT *
FROM Employees
WHERE Department = 'IT';
Enter fullscreen mode Exit fullscreen mode

This is why understanding the SQL generated by EF Core is so important.

A query that looks elegant in C# isn't automatically efficient.


2. Select Only the Columns You Need

One of the easiest ways to improve database performance is to avoid retrieving unnecessary columns.

Consider:

var employees = await context.Employees
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

This may retrieve every column from the Employees table.

But suppose your API only needs:

  • Id
  • Name
  • Department

There is no reason to retrieve:

  • Salary
  • Address
  • DateOfBirth
  • ProfileImage
  • CreatedDate
  • UpdatedDate

Instead, use projection.

var employees = await context.Employees
    .Select(e => new
    {
        e.Id,
        e.Name,
        e.Department
    })
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Now the database only needs to return the required fields.

This can significantly reduce:

  • Network traffic
  • Database I/O
  • Memory usage
  • Object materialization
  • API response processing

Real-World Example

Imagine an employee table contains 40 columns and 500,000 records.

If your API only needs three columns, retrieving complete entities unnecessarily can become expensive.

Projection allows the database to do less work.

Better API Design

You can also project directly into a DTO:

var employees = await context.Employees
    .Select(e => new EmployeeDto
    {
        Id = e.Id,
        Name = e.Name,
        Department = e.Department
    })
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

This is generally preferable for API responses because you're explicitly controlling the data exposed by your application.


3. Use AsNoTracking() for Read-Only Queries

EF Core normally keeps track of entities it retrieves.

This tracking is useful when you're going to modify those entities.

For example:

var employee = await context.Employees
    .FirstAsync(e => e.Id == id);

employee.Salary = 100000;

await context.SaveChangesAsync();
Enter fullscreen mode Exit fullscreen mode

EF Core needs to know that the entity changed.

But what if you're only reading data?

For example:

var employees = await context.Employees
    .AsNoTracking()
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

AsNoTracking() tells EF Core:

I only need these entities for reading. Don't track them for changes.

This can reduce tracking overhead for read-heavy workloads.

When Should You Use It?

Good candidates include:

  • Search endpoints
  • Reporting APIs
  • Dashboard queries
  • Product listings
  • Read-only pages
  • Lookup data

For example:

var products = await context.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

When Should You Avoid It?

Don't blindly use AsNoTracking() when you intend to modify the entity and call SaveChanges().

For example:

var employee = await context.Employees
    .AsNoTracking()
    .FirstAsync(e => e.Id == id);

employee.Salary = 90000;

await context.SaveChangesAsync();
Enter fullscreen mode Exit fullscreen mode

The entity isn't being tracked, so EF Core won't automatically detect that change in the usual way.

Use tracking when you actually need change tracking.


4. Always Consider Pagination

One of the most common database performance mistakes is returning every record.

Imagine an endpoint:

GET /api/products
Enter fullscreen mode Exit fullscreen mode

Your database contains 2 million products.

This is a terrible idea:

var products = await context.Products
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

You're asking the application to retrieve potentially millions of rows.

Instead, use pagination.

int page = 1;
int pageSize = 20;

var products = await context.Products
    .OrderBy(p => p.Id)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

For page 1:

Skip = 0
Take = 20
Enter fullscreen mode Exit fullscreen mode

For page 2:

Skip = 20
Take = 20
Enter fullscreen mode Exit fullscreen mode

For page 3:

Skip = 40
Take = 20
Enter fullscreen mode Exit fullscreen mode

Why Pagination Matters

Pagination reduces:

  • Database result size
  • Network traffic
  • Application memory usage
  • Serialization time
  • API response size

For large datasets, pagination should be considered part of API design rather than an afterthought.


5. Avoid the N+1 Query Problem

The N+1 query problem is one of the most important EF Core performance issues to understand.

Suppose you have:

Orders
  ↓
Customer
Enter fullscreen mode Exit fullscreen mode

You retrieve 100 orders:

var orders = await context.Orders
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Then inside a loop, you retrieve the customer:

foreach (var order in orders)
{
    var customer = await context.Customers
        .FirstAsync(c => c.Id == order.CustomerId);
}
Enter fullscreen mode Exit fullscreen mode

You might accidentally execute:

1 query for Orders
+
100 queries for Customers
=
101 database queries
Enter fullscreen mode Exit fullscreen mode

That's the N+1 problem.

Better Approach

Use an appropriate query that retrieves the required information together.

For example:

var orders = await context.Orders
    .Select(o => new
    {
        o.Id,
        o.OrderDate,
        CustomerName = o.Customer.Name
    })
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Now EF Core can translate this relationship into SQL and retrieve the required data more efficiently.

The important lesson is:

Be careful when accessing related data inside loops.


6. Be Careful With Include()

Include() is useful when you need related entities.

For example:

var orders = await context.Orders
    .Include(o => o.Customer)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

This tells EF Core to load the related customer.

That can be appropriate.

But don't automatically add Include() everywhere.

For example:

var orders = await context.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
    .Include(o => o.Payments)
    .Include(o => o.ShippingAddress)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

You may end up retrieving a very large amount of related data.

Instead, ask:

Do I actually need these related entities?

If your API only needs the customer name, projection may be better:

var orders = await context.Orders
    .Select(o => new
    {
        o.Id,
        o.OrderDate,
        CustomerName = o.Customer.Name
    })
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

The goal isn't to avoid Include().

The goal is to use it intentionally.


7. Prefer Projection with Select()

Projection is one of the most useful techniques in EF Core.

Consider:

var users = await context.Users
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

You're loading complete entities.

But perhaps your UI only needs:

Id
Name
Email
Enter fullscreen mode Exit fullscreen mode

Instead:

var users = await context.Users
    .Select(u => new UserDto
    {
        Id = u.Id,
        Name = u.Name,
        Email = u.Email
    })
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

This approach has several benefits.

1. Less Data

Only required columns are retrieved.

2. Better API Contracts

The API explicitly defines what it returns.

3. Less Memory

The application doesn't need to materialize unnecessary entity properties.

4. Better Security

You're less likely to accidentally expose fields that shouldn't be returned.

For example, don't return an entire user entity if it contains:

PasswordHash
SecurityStamp
InternalNotes
ResetToken
Enter fullscreen mode Exit fullscreen mode

Instead, explicitly select what the API needs.


8. Use Database Indexes

EF Core query optimization isn't only about C#.

Your database design matters enormously.

Suppose you frequently execute:

var employees = await context.Employees
    .Where(e => e.Email == email)
    .FirstOrDefaultAsync();
Enter fullscreen mode Exit fullscreen mode

If Email isn't indexed, the database may need to scan a large number of rows.

A database index can make lookups much faster.

For example, using EF Core configuration:

modelBuilder.Entity<Employee>()
    .HasIndex(e => e.Email)
    .IsUnique();
Enter fullscreen mode Exit fullscreen mode

This creates an index on the email column.

Composite Indexes

Sometimes queries filter on multiple columns.

For example:

var orders = await context.Orders
    .Where(o =>
        o.CustomerId == customerId &&
        o.Status == "Completed")
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

A composite index may be useful:

modelBuilder.Entity<Order>()
    .HasIndex(o => new
    {
        o.CustomerId,
        o.Status
    });
Enter fullscreen mode Exit fullscreen mode

But don't create indexes for every column.

Indexes have costs too.

They can:

  • Consume storage
  • Increase insert/update overhead
  • Increase maintenance cost

Indexes should be based on actual query patterns.


9. Don't Call ToList() Too Early

This is another common LINQ and EF Core mistake.

Consider:

var employees = await context.Employees
    .ToListAsync();

var result = employees
    .Where(e => e.Department == "IT")
    .ToList();
Enter fullscreen mode Exit fullscreen mode

The first ToListAsync() executes the database query immediately.

That means the application retrieves all employees before filtering them.

A better approach is:

var result = await context.Employees
    .Where(e => e.Department == "IT")
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Now the filtering happens at the database level.

Conceptually:

Less Efficient

Database
   ↓
All Employees
   ↓
Application
   ↓
Filter
Enter fullscreen mode Exit fullscreen mode

Better

Database
   ↓
Filter
   ↓
Only IT Employees
   ↓
Application
Enter fullscreen mode Exit fullscreen mode

This is one of the most important concepts when working with IQueryable.


10. Inspect the SQL Generated by EF Core

Never assume your LINQ query produces the SQL you expect.

EF Core provides ways to inspect generated SQL.

For example:

var query = context.Employees
    .Where(e => e.Department == "IT");

Console.WriteLine(query.ToQueryString());
Enter fullscreen mode Exit fullscreen mode

This can show the SQL generated for the query.

This is incredibly useful when diagnosing:

  • Slow queries
  • Unexpected joins
  • Missing filters
  • Excessive columns
  • Unexpected SQL generation

Example

You may write:

var result = context.Employees
    .Where(e => e.Salary > 80000)
    .OrderBy(e => e.Name);
Enter fullscreen mode Exit fullscreen mode

Instead of guessing what happens, inspect the generated SQL.

Then you can take the SQL to your database tools and analyze the execution plan.

Remember

EF Core is an abstraction over the database.

When performance matters, you should understand both:

C# / LINQ
Enter fullscreen mode Exit fullscreen mode

and:

SQL / Database Execution
Enter fullscreen mode Exit fullscreen mode

Being comfortable with both makes you a much stronger .NET developer.


11. Combine Techniques for Real Applications

The biggest performance improvements usually come from combining multiple techniques.

Suppose you're building an employee search API.

A good query might look like:

var employees = await context.Employees
    .AsNoTracking()
    .Where(e => e.IsActive)
    .Where(e => e.Department == department)
    .OrderBy(e => e.Name)
    .Select(e => new EmployeeDto
    {
        Id = e.Id,
        Name = e.Name,
        Department = e.Department
    })
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

This single query demonstrates several useful techniques:

  • AsNoTracking() for read-only data
  • Filtering at the database
  • Ordering
  • Projection
  • Pagination
  • Async database access

That's much better than retrieving thousands of complete entities and processing them inside application code.


12. Common EF Core Performance Mistakes

Here are some mistakes worth checking in your own projects.

❌ Loading Everything

context.Products.ToListAsync();
Enter fullscreen mode Exit fullscreen mode

when the application only needs 20 records.

❌ Selecting Complete Entities

context.Users.ToListAsync();
Enter fullscreen mode Exit fullscreen mode

when the API only needs three properties.

❌ Filtering After ToList()

context.Users
    .ToList()
    .Where(...);
Enter fullscreen mode Exit fullscreen mode

❌ Unnecessary Tracking

Using tracked entities for large read-only reports.

❌ N+1 Queries

Executing database queries inside loops.

❌ Too Many Includes

Loading a huge object graph when only a few fields are required.

❌ Ignoring Database Indexes

Filtering and sorting frequently on columns that aren't appropriately indexed.

❌ Ignoring Generated SQL

Assuming that clean LINQ automatically means efficient SQL.


13. Practical EF Core Performance Checklist

Before releasing an API that uses EF Core, ask yourself:

☐ Am I selecting only the columns I need?

☐ Is this query read-only?

☐ Should I use AsNoTracking()?

☐ Does this endpoint need pagination?

☐ Am I accidentally creating N+1 queries?

☐ Do I really need every Include()?

☐ Can I use projection with Select()?

☐ Are frequently filtered columns indexed?

☐ Am I calling ToList() too early?

☐ Have I inspected the generated SQL?

☐ Have I tested the query with realistic data volumes?

☐ Have I checked the database execution plan?
Enter fullscreen mode Exit fullscreen mode

The last two are especially important.

A query that performs well with 1,000 records may behave very differently with 10 million records.

Always test performance using data volumes that resemble production.


14. EF Core Interview Questions

What is AsNoTracking()?

AsNoTracking() tells EF Core not to track returned entities for changes. It is commonly useful for read-only queries.

Why is Select() useful for performance?

It allows you to retrieve only the fields required by the application instead of loading complete entities.

What is the N+1 query problem?

It occurs when an application executes one query to retrieve a collection and then executes additional queries for each item in that collection.

Why is ToList() important?

ToList() materializes the query and generally causes the database query to execute immediately.

Calling it too early can cause unnecessary data to be retrieved.

Does EF Core automatically create indexes?

EF Core creates indexes for certain conventions, such as primary keys, but application-specific indexes often need to be configured intentionally.

What is projection?

Projection means selecting only the data required from an entity, often using Select().

Why should generated SQL be inspected?

Because the LINQ expression you write doesn't always make the resulting SQL obvious. Inspecting SQL helps identify inefficient queries, joins, missing filters, and other database performance issues.


15. Frequently Asked Questions

Is EF Core slower than writing SQL manually?

Not necessarily.

EF Core can generate efficient SQL for many common scenarios, but developers still need to understand how their queries translate to SQL.

For highly specialized queries, raw SQL or other database-specific approaches can sometimes be appropriate.

The important thing is to measure rather than assume.

Should I always use AsNoTracking()?

No.

Use it when you don't need change tracking.

If you're retrieving an entity specifically to modify and save it, normal tracking may be appropriate.

Should I avoid Include()?

No.

Include() is useful when you genuinely need related entities.

The problem is using it without considering how much data you're loading.

Is Select() always faster?

Not automatically.

Projection can reduce the amount of data retrieved, but the actual performance depends on the query, database, indexes, relationships, and data size.

Measure important queries.

How can I find slow EF Core queries?

Start by logging SQL generated by EF Core and inspecting the queries using your database's execution-plan tools.

Application performance monitoring and database monitoring can also help identify slow queries in production.


16. Final Thoughts

Entity Framework Core allows .NET developers to work with databases using familiar C# and LINQ.

But abstraction doesn't eliminate the need to understand what's happening underneath.

The most important performance lessons from this article are:

  1. Retrieve only the data you need.
  2. Use AsNoTracking() for appropriate read-only queries.
  3. Paginate large datasets.
  4. Watch out for N+1 queries.
  5. Use Include() intentionally.
  6. Prefer projection when you only need specific fields.
  7. Design appropriate database indexes.
  8. Don't materialize queries too early.
  9. Inspect the SQL generated by EF Core.
  10. Test with realistic production-sized data.

The biggest mistake is optimizing based on assumptions.

Instead of thinking:

"This LINQ query looks simple, so it must be fast."

Think:

"What SQL will this generate, how will the database execute it, and how will it behave when the data grows?"

That mindset will help you write significantly better ASP.NET Core applications.


🚀 Explore More Free Developer Tools

As developers, we spend a surprising amount of time working with API responses, configuration files, tokens, test data, and different data formats while building applications.

That's one of the reasons I built ToolBenchApp.

👉 https://toolbenchapp.com/

ToolBenchApp is a growing collection of free, browser-based developer tools designed to simplify everyday development tasks.

For .NET and API developers, some particularly useful tools include:

  • JSON Formatter & Validator
  • JSON ↔ XML Converter
  • JSON ↔ CSV Converter
  • JWT Decoder
  • Base64 Encoder/Decoder
  • UUID Generator
  • URL Encoder/Decoder
  • SQL Formatter
  • YAML Formatter
  • Text Difference Checker
  • HTML Formatter
  • Regex Tester

For example, when debugging an ASP.NET Core API, you can quickly format a JSON response, inspect a JWT, compare two API payloads, or format SQL without installing another application.

I'm continuously adding new utilities to ToolBenchApp based on practical developer needs. If there's a small developer task you repeatedly perform and would like to turn into a free browser-based tool, I'd love to hear your suggestion.

Happy coding! 🚀

Top comments (0)