Introduction
LINQ is one of the most productive parts of the .NET ecosystem, but that productivity can hide expensive mistakes. A query that looks elegant at first glance can still be wasteful, hard to maintain, or translated into inefficient SQL.
Here are the mistakes I see most often, plus the habits that prevent them.
1. Forcing execution too early
Deferred execution is powerful because the query is not evaluated until the data is actually needed. The problem starts when ToList(), ToArray(), or ToDictionary() is called too early.
var query = context.Customers
.Where(c => c.IsActive)
.OrderBy(c => c.Name);
foreach (var customer in query)
{
Console.WriteLine(customer.Name);
}
Better habit
- Keep queries composable for as long as possible.
- Materialize only when you truly need in-memory results.
2. Overusing Select
Projection is essential, but unnecessary intermediate projections make code noisier and can make translation harder to reason about.
var names = context.Customers
.Where(c => c.IsActive)
.Select(c => c.Name)
.ToList();
Better habit
- Project only once when possible.
- Keep the projection aligned with the actual shape needed by the consumer.
3. Ignoring performance implications
LINQ can become expensive quickly when large datasets are involved.
Common performance traps:
- multiple enumerations of the same sequence,
- heavy client-side logic inside the query,
- reading more columns than needed,
- and letting work happen in memory instead of in the database.
var highValueCustomers = context.Customers
.Where(c => c.PurchaseAmount > 1000)
.ToList();
Better habit
- Prefer database-side filtering and projection.
- Profile query-heavy flows instead of guessing.
- Validate the generated SQL when the query matters.
4. Fetching entire entities when a projection is enough
If the UI needs only a few properties, reading full entities increases memory usage and transfer cost for no benefit.
var customerNames = context.Customers
.Where(c => c.IsActive)
.Select(c => new { c.Id, c.Name })
.ToList();
Better habit
- Use lightweight projections for lists, dashboards, and summaries.
- Reserve full entities for cases that genuinely need the full object graph.
5. Writing overly complex queries
Large LINQ expressions are difficult to test, explain, and optimize. They also increase the chance of poor SQL generation when used with an ORM.
var activeCustomers = context.Customers.Where(c => c.IsActive);
var highValueCustomers = activeCustomers.Where(c => c.PurchaseAmount > 1000);
Better habit
- Break complex logic into named query fragments.
- Extract reusable patterns into helper methods or extension methods.
6. Not handling nulls explicitly
Null-related surprises are easy to miss, especially when values come from optional relationships or partially populated datasets.
var customerNames = context.Customers
.Select(c => c.Name ?? "Unknown")
.ToList();
Better habit
- Use null-coalescing where defaults are acceptable.
- Be explicit about optional relationships instead of relying on luck.
7. Forgetting database behavior
A readable LINQ query is not automatically a good SQL query. The abstraction is useful, but it does not remove the need to understand joins, includes, indexes, or N+1 problems.
var customers = context.Customers
.Include(c => c.Orders)
.Where(c => c.IsActive)
.ToList();
Better habit
- Use
Includeintentionally. - Watch for over-fetching.
- Verify that indexes and query shape support the access pattern.
8. Misusing GroupBy
Grouping in memory when the real goal is grouped database output can turn a manageable query into an unnecessary memory problem.
var customerGroups = context.Customers
.GroupBy(c => c.City)
.Select(g => new { City = g.Key, Count = g.Count() })
.ToList();
Better habit
- Keep grouping in the database when possible.
- Double-check whether the provider can translate the expression as expected.
9. Skipping asynchronous methods
In I/O-bound application code, synchronous database calls reduce scalability and block useful work.
var customers = await context.Customers
.Where(c => c.IsActive)
.ToListAsync();
Better habit
- Prefer async query APIs in web applications and service layers.
- Stay consistent so the entire call chain remains non-blocking.
10. Repeating the same query intent everywhere
If the same filter appears repeatedly, the issue is usually not LINQ syntax. It is missing abstraction.
public static class CustomerExtensions
{
public static IQueryable<Customer> ActiveCustomers(this IQueryable<Customer> query)
{
return query.Where(c => c.IsActive);
}
}
Better habit
- Encapsulate recurring query patterns.
- Keep your business language visible in the query surface.
Conclusion
LINQ works best when elegance and discipline go together. Respect deferred execution, project only what you need, keep database behavior in mind, and extract repeated intent into reusable patterns.
That combination gives you LINQ queries that are not just readable, but also efficient and maintainable.