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.