Entity Framework  

Common Mistakes Developers Make in EF Core : How to Avoid Them

Introduction

In this article, we will learn about Common Mistakes Developers make in EF Core.

Before we start, please take a look at my last article on Entity Framework.

Now, let get started.

Entity Framework Core (EF Core) is a powerful ORM for .NET developers, but it’s easy to fall into pitfalls that hurt app performance, maintainability, or correctness. Whether you’re building an ASP.NET Core web app, a desktop client, or a microservice, these common EF Core mistakes often pop up.

Here’s a concise guide to the most frequent EF Core mistakes, complete with code samples and tips for writing better data access code.

Forgetting to Use AsNoTracking() for Read-Only Queries

By default, EF Core tracks the entities it loads to monitor changes — but tracking costs memory and CPU cycles.

If you only need to read data without modifying it, use AsNoTracking() to skip change tracking and improve query performance.

// Inefficient: tracks every entity
var users = context.Users.ToList();

// Better: no tracking for read-only scenarios
var users = context.Users.AsNoTracking().ToList();

This small change can reduce overhead drastically when querying large result sets.

Loading Too Little or Too Much Data: Proper Use of Include()

EF Core doesn’t automatically load related entities unless lazy loading is enabled (which isn’t on by default).

Failing to eager load related data causes missing information, but eager loading everything can overload your queries.

Don’t do this

// Orders loaded without related Customer or OrderDetails

var orders = context.Orders.ToList();

Do this

// Explicitly include related data you need

var orders = context.Orders
    .Include(o => o.Customer)
    .Include(o => o.OrderDetails)
    .ToList();

Only include navigation properties you actually need.

Querying Inside Loops — The N+1 Query Problem

Writing queries inside loops leads to executing multiple queries - one for each iteration - causing serious performance issues.

foreach (var id in orderIds)
{
    var order = context.Orders.FirstOrDefault(o => o.Id == id);
}

Better approach: use a single query to fetch all needed entities at once.

var orders = context.Orders
    .Where(o => orderIds.Contains(o.Id))
    .ToList();

This reduces roundtrips to the database and improves speed.

Using Synchronous Queries in Asynchronous Environments

Using blocking methods like .ToList() in ASP.NET Core apps can cause thread pool starvation and scalability issues.

var users = context.Users.ToList(); // Blocks thread

Always prefer async APIs.

var users = await context.Users.ToListAsync();

Async EF Core methods keep your app responsive under load.

Incorrect DbContext Lifetime Configuration

DbContext is not thread-safe and should not be registered as a singleton in dependency injection.

Incorrect

services.AddSingleton<AppDbContext>();

Correct

services.AddDbContext<AppDbContext>(); // Scoped by default

Scoped lifetime matches the lifetime of an HTTP request, ensuring safe concurrent use.

Not Using Transactions for Multiple Related Operations

Saving multiple related entities in separate SaveChanges() calls risks leaving your database in an inconsistent state if one operation fails.

context.Add(order);
await context.SaveChangesAsync();

context.Add(payment);
await context.SaveChangesAsync();

Instead, use a transaction.

using var transaction = await context.Database.BeginTransactionAsync();
try
{
    context.Add(order);
    context.Add(payment);
    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

Applying Filters After Materializing Queries (ToList())

Calling .ToList() brings all data into memory. If you apply filters after, you’re filtering in memory instead of in the database - potentially loading tons of unnecessary data.

// Bad: filters applied in memory

var orders = context.Orders.ToList()
    .Where(o => o.Total > 1000);
// Good: filters applied in SQL

var orders = context.Orders
    .Where(o => o.Total > 1000)
    .ToList();

Bonus Tips

  • Use .AsNoTracking() on queries where you won’t modify data.
  • Configure .Include() carefully to avoid loading unwanted data.
  • Generate and share .editorconfig files to enforce consistent code style and conventions.
  • Use EF Core migrations to manage schema changes safely.
  • Handle concurrency conflicts using concurrency tokens like [Timestamp] fields.

Conclusion

EF Core is incredibly flexible and efficient when used correctly. Avoid these common pitfalls to keep your application performant, scalable, and maintainable. Incorporate these best practices early to save headaches down the road.