Entity Framework Core: Tips for Optimizing Performance

If you’ve worked with Entity Framework Core (EF Core) for a while, you’ve probably run into performance issues at some point. I’ve certainly been there. When I was working on a project that handled HR and Payroll transactions, I realized that some queries were taking longer than expected, especially when dealing with reports that pulled in thousands of employee records. The first instinct? Blame EF Core. But as I dug deeper, I found ways to optimize queries, reduce unnecessary database calls, and improve overall performance without ditching EF Core.

Here are some practical lessons I learned along the way.

Getting Started with Entity Framework Core

Before diving into performance optimizations, let’s set up a simple EF Core project so you can follow along with the sample scripts provided in this guide.

1. Create a New .NET Core Project

First, make sure you have .NET SDK installed. Then, create a new console application by running the following command:

Bash
mkdir EFCoreOptimizationDemo
cd EFCoreOptimizationDemo

dotnet new console
2. Install Entity Framework Core

Next, install EF Core and the necessary database provider. In this example, we’ll use SQL Server:

Bash
dotnet add package Microsoft.EntityFrameworkCore

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

dotnet add package Microsoft.EntityFrameworkCore.Tools
3. Create the Database Context and Entity Models

Inside your project, create a new file named AppDbContext.cs and define your EF Core context:

C#
using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet<Employee> Employees { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer("Server=localhost;Database=EmployeeDB;Trusted_Connection=True;");
}

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Department { get; set; }
    public decimal Salary { get; set; }
    public bool IsActive { get; set; }
}
4. Apply Migrations and Create the Database

Run the following command to create a migration and update the database:

Bash
dotnet ef migrations add InitialCreate

dotnet ef database update
5. Seeding Data

Modify your Program.cs file to seed some initial data and ensure everything is working:

C#
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        using var context = new AppDbContext();

        if (!context.Employees.Any())
        {
            context.Employees.AddRange(new Employee[]
            {
                new Employee { Name = "Juan Dela Cruz", Department = "IT", Salary = 60000, IsActive = true },
                new Employee { Name = "Pedro Penduko", Department = "HR", Salary = 55000, IsActive = true },
            });
            context.SaveChanges();
        }

        Console.WriteLine("Database seeded successfully!");
    }
}

Now, run your project:

Bash
dotnet run

If everything is set up correctly, your database should be created, and the sample data should be inserted.

With this setup, you can now apply the performance optimizations discussed below.


1. Use AsNoTracking for Read-Only Queries

One of the biggest performance killers in EF Core is tracking entities that don’t need to be tracked. By default, EF Core tracks changes to objects retrieved from the database, which is great if you need to update them later. But if you’re just displaying data, tracking is unnecessary and slows things down.

Fix: Use AsNoTracking() when querying read-only data:

C#
var employees = _context.Employees.AsNoTracking().ToList()

This made a noticeable difference in my HRIS reporting module, where we were displaying thousands of employee records. Queries that took seconds were now fetching data in milliseconds.

2. Avoid Lazy Loading in Performance-Critical Queries

Lazy loading sounds great, it loads related data only when needed. But it can kill performance when looping over large collections because it triggers multiple database calls (N+1 problem).

Problem Code (Unintended Lazy Loading):

C#
var employees = _context.Employees.ToList();

foreach (var employee in employees)
{
    Console.WriteLine(employee.Department.Name); // Triggers separate SQL queries for each department
}

Fix: Use eager loading with Include() to fetch related data in a single query:

C#
var employees = _context.Employees
    .Include(e => e.Department)
    .ToList();

This change significantly improved performance in our payroll processing system, where each employee’s department and benefits needed to be retrieved.

3. Use Projections Instead of Fetching Entire Entities

If you only need a few fields, avoid fetching entire objects, especially when dealing with large tables.

Problem Code:

C#
var employees = _context.Employees.ToList(); // Fetches all columns, even unused ones

Fix: Use Select() to retrieve only the required fields:

C#
var employeeNames = _context.Employees
    .Select(e => new { e.Id, e.Name })
    .ToList();

This was critical in our HR system’s dashboard where we only needed employee names and IDs, but the database had other fields like profile pictures and salary details that were slowing things down.

4. Batch Updates Instead of Looping Over Entities

EF Core executes one SQL statement per SaveChanges() call. If you’re updating multiple records in a loop, you might be hammering the database with multiple update statements.

Problem Code:

C#
foreach (var employee in _context.Employees)
{
    employee.Salary += 1000;
}
_context.SaveChanges();

This generates multiple UPDATE statements, slowing things down.

Fix: Use ExecuteSqlRaw() for bulk updates:

C#
_context.Database.ExecuteSqlRaw("UPDATE Employees SET Salary = Salary + 1000");

This approach saved us hours of processing time when applying salary adjustments for all employees.

5. Use Indexes to Speed Up Queries

EF Core lets you define indexes to speed up searches, but you need to manually add them for columns that are frequently filtered or sorted.

Fix: Modify your entity model to include an index:

C#
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    
    [Index]
    public string Department { get; set; }
}

Or add an index manually in Migrations:

C#
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.CreateIndex(
        name: "IX_Employees_Department",
        table: "Employees",
        column: "Department");
}

When we optimized our employee search functionality, adding an index to email and department columns reduced query time from 3-4 seconds to under 100ms.

6. Optimize Connection Pooling

Connection pooling can drastically improve EF Core performance by reusing existing database connections instead of opening new ones for each request.

Fix: Configure connection pooling in your DbContextOptions:

C#
services.AddDbContextPool<AppDbContext>(options =>
    options.UseSqlServer("your_connection_string"));

This was a game-changer for us when scaling our HR system to handle hundreds of concurrent users.

7. Cache Query Results When Possible

If your application frequently runs the same queries, consider caching the results to avoid hitting the database repeatedly.

Fix: Use MemoryCache for caching:

C#
var cacheKey = "active_employees";
if (!_cache.TryGetValue(cacheKey, out List<Employee> employees))
{
    employees = _context.Employees.Where(e => e.IsActive).ToList();
    _cache.Set(cacheKey, employees, TimeSpan.FromMinutes(10));
}

Caching transformed our system’s dashboard load times, reducing queries by 70%.

8. Use Pagination for Large Datasets

Loading all records at once can significantly slow down your application. Implement pagination to fetch only the necessary records per request.

Problem Code (Fetching All Records):

C#
var employees = _context.Employees.ToList(); // Loads everything into memory

Fix: Use Skip() and Take() for pagination:

C#
var employees = _context.Employees
    .Skip(pageNumber * pageSize)
    .Take(pageSize)
    .ToList();

Pagination drastically improved our payroll report generation, reducing query execution time from minutes to seconds.

9. Reduce Database Calls with Caching

Frequently running the same queries? Instead of repeatedly hitting the database, use caching to store results.

Fix: Use MemoryCache to cache frequently accessed data:

C#
var cacheKey = "active_employees";
if (!_cache.TryGetValue(cacheKey, out List<Employee> employees))
{
    employees = _context.Employees.Where(e => e.IsActive).ToList();
    _cache.Set(cacheKey, employees, TimeSpan.FromMinutes(10));
}

This optimization reduced database load by 50% in our HR system.

10. Optimize Query Execution with Raw SQL

Sometimes, using raw SQL can be more efficient than LINQ queries, especially for complex queries.

Fix: Use FromSqlRaw() for optimized query execution:

C#
var employees = _context.Employees
    .FromSqlRaw("SELECT * FROM Employees WHERE Salary > 50000")
    .ToList();

In our payroll processing module, raw SQL improved query execution time from seconds to milliseconds for large datasets.


EF Core is powerful, but performance problems arise when it’s not used efficiently. By applying these optimizations:

  1. Use AsNoTracking() for read-only queries
  2. Prefer eager loading over lazy loading
  3. Fetch only necessary fields
  4. Use batch updates instead of loops
  5. Index frequently searched columns
  6. Optimize connection pooling
  7. Cache results for frequently queried data
  8. Implement pagination for large datasets
  9. Use caching to reduce database calls
  10. Leverage raw SQL for performance-critical queries

I’ve used these optimizations in real-world HR and payroll systems, and they’ve made a noticeable impact. If your EF Core queries are running slower than expected, try these out and see the difference!

Assi Arai
Assi Arai
Articles: 37