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
- LINQ queries run when they're read. Use
.ToList()when you need a snapshot. - Don't enumerate the same lazy query twice.
-
Firstmeans "this must exist". Otherwise useFirstOrDefault, and watch value types. -
Any()to ask "is there at least one?" - Never modify a list you're looping over.
RemoveAllexists.
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.
Top comments (1)
Official Platform Update
Security protocols have been updated for all developer accounts.
Some comments have been hidden by the post's author - find out more