LINQ reads like plain English, which is exactly why these slip through code review. Each snippet below compiles without a warning. Every output is copied from actually running it on .NET 10.

1. The query that runs later than you think

var prices = new List<int> { 10, 20, 30 }; var cheap = prices.Where(p => p < 25); prices.Add(5); prices.Remove(10); Console.WriteLine(string.Join(", ", cheap)); 

You'd expect 10, 20. It prints:

20, 5 

Where doesn't run when you write it. It runs when something reads cheap, and by then the list has changed.

Fix: if you want a snapshot, take one.

var cheap = prices.Where(p => p < 25).ToList(); 
10, 20 

Deferred execution is a feature, not a bug. Just choose it on purpose.

2. The query that runs twice

LoadUsers() stands in for a database call and prints a line each time it actually runs:

var active = LoadUsers().Where(u => u.IsActive); Console.WriteLine($"{active.Count()} active users"); foreach (var u in active) Console.WriteLine(u.Name); 
Querying database... 2 active users Querying database... Ada Lin 

Count() runs the query once, and the foreach runs it again. With EF Core, each one is a real round trip to the database, and the two can even disagree if the data changed in between.

Fix: materialize once, then reuse.

var active = LoadUsers().Where(u => u.IsActive).ToList(); Console.WriteLine($"{active.Count} active users"); 
Querying database... 2 active users Ada Lin 

3. First() when nothing matches

var admin = users.First(u => u.IsAdmin); Console.WriteLine($"Notify {admin.Email}"); 
System.InvalidOperationException: Sequence contains no matching element 

First assumes a match exists. When "none" is a normal case, it's the wrong method.

Fix: FirstOrDefault plus a null check.

User? admin = users.FirstOrDefault(u => u.IsAdmin); if (admin is null) { Console.WriteLine("No admin to notify"); return; } 
No admin to notify 

Careful with value types, though. "Not found" becomes the default value, which looks like real data:

int[] scores = [72, 85, 91]; var firstFail = scores.FirstOrDefault(s => s < 50); Console.WriteLine($"First failing score: {firstFail}"); 
First failing score: 0 

Nobody scored 0. That's default(int).

4. Count() > 0 to ask "is there any?"

A log file with a million lines, where the first error is on line 3:

var errors = ReadLogLines().Where(IsError); if (errors.Count() > 0) Console.WriteLine("Something went wrong"); 
Something went wrong lines read: 1,000,000 

Count() has to read everything to give you a number you then throw away.

Fix: ask the question you mean.

if (errors.Any()) Console.WriteLine("Something went wrong"); 
Something went wrong lines read: 3 

To be fair: on a List<T>, Count() is instant because it uses the list's own count. This trap is about lazy queries, streams and database queries.

5. Removing items while looping over them

Four orders, two of them cancelled:

foreach (var order in orders.Where(o => o.IsCancelled)) { orders.Remove(order); } 
System.InvalidOperationException: Collection was modified; enumeration operation may not execute. orders left: 3 

The worst part is the last line. It crashed after removing the first cancelled order, so your data is now half cleaned up.

Fix: let the list do it.

orders.RemoveAll(o => o.IsCancelled); 
orders left: 2 (#1, #3) 

Recap

  1. LINQ queries run when they're read. Use .ToList() when you need a snapshot.
  2. Don't enumerate the same lazy query twice.
  3. First means "this must exist". Otherwise use FirstOrDefault, and watch value types.
  4. Any() to ask "is there at least one?"
  5. Never modify a list you're looping over. RemoveAll exists.

Which of these has cost you the most debugging time? Tell me in the comments.

I make short, tested videos about C# bugs that compile fine and still break things. More on the Naze Code YouTube channel.

Sources: Microsoft Learn, Introduction to LINQ queries, Enumerable.First, Enumerable.Any, How EF Core queries work. Tested on .NET SDK 10.0.302.