LINQ Common Mistakes in C#: 10 Problems Every .NET Developer Should Avoid




 

LINQ is one of the features that makes C# incredibly productive.

With just a few lines of code, you can filter collections, transform objects, group data, sort results, and query databases.

But there's a catch.

LINQ code can look clean while doing something completely different from what you intended.

A query that looks harmless can execute multiple times, load thousands of database records into memory, throw an exception because of an empty sequence, or silently produce the wrong result.

In this article, we'll look at 10 common LINQ mistakes in C#, why they happen, and how to write better alternatives.


1. Calling ToList() Too Early

One of the most common LINQ mistakes is materializing a query before you're finished building it.

Consider:

var users = db.Users
    .ToList()
    .Where(u => u.IsActive)
    .ToList();

At first glance, this looks perfectly reasonable.

But ToList() executes the database query immediately.

That means the application may retrieve every user from the database, and only then filter the active users in memory.

Better

var users = db.Users
    .Where(u => u.IsActive)
    .ToList();

Now the filtering can be translated into SQL and performed by the database.

Conceptually, you're moving from:

Database
   ↓
All Users
   ↓
Application Memory
   ↓
Filter

to:

Database
   ↓
WHERE IsActive = 1
   ↓
Only required users
   ↓
Application Memory

This can make a significant difference when working with large tables.

Rule

Delay ToList(), ToArray(), and similar materialization methods until you actually need the results in memory.


2. Enumerating the Same LINQ Query Multiple Times

Consider:

var activeUsers = users.Where(u => u.IsActive);

if (activeUsers.Any())
{
    Console.WriteLine(activeUsers.Count());
}

The query is potentially evaluated multiple times.

First:

activeUsers.Any()

Then:

activeUsers.Count()

If users represents an expensive data source, this can become inefficient.

Better

If you need the collection multiple times, materialize it once:

var activeUsers = users
    .Where(u => u.IsActive)
    .ToList();

if (activeUsers.Count > 0)
{
    Console.WriteLine(activeUsers.Count);
}

Or, if you only need to know whether there are elements:

if (activeUsers.Any())
{
    ...
}

Important distinction

Don't automatically replace:

Any()

with:

Count() > 0

Any() is generally preferable when you only need to know whether at least one element exists because it can stop after finding the first matching element.


3. Using Count() When You Only Need to Check for Existence

This mistake is extremely common:

if (users.Count() > 0)
{
    // ...
}

If your intention is simply:

"Does at least one user exist?"

use:

if (users.Any())
{
    // ...
}

Even better when you have a condition:

if (users.Any(u => u.IsActive))
{
    // At least one active user exists
}

Instead of:

if (users.Count(u => u.IsActive) > 0)
{
    // ...
}

The second version potentially counts every matching element.

The first version can stop as soon as it finds one.

Prefer

Any()

for existence checks.

Use:

Count()

when you actually need the count.


4. Using First() When the Sequence Might Be Empty

Consider:

var user = users.First(u => u.Id == userId);

What happens if no user exists?

You get:

InvalidOperationException

That might be perfectly acceptable if the application guarantees that the user exists.

But if the record is optional, use:

var user = users.FirstOrDefault(u => u.Id == userId);

Then handle the missing result:

var user = users.FirstOrDefault(u => u.Id == userId);

if (user is null)
{
    return NotFound();
}

First() vs FirstOrDefault()

Use:

First()

when an empty sequence represents an exceptional condition.

Use:

FirstOrDefault()

when no result is a normal possibility.


5. Confusing First() With Single()

These two methods are not interchangeable.

Consider:

var user = users.First(u => u.Email == email);

First() means:

Give me the first matching item.

If five records match, it simply returns the first one.

Now consider:

var user = users.Single(u => u.Email == email);

Single() means:

There must be exactly one matching item.

It throws if:

  • there are no matches

  • there are multiple matches

This can actually be useful when your business rule requires uniqueness.

For example:

var account = accounts.Single(a => a.AccountNumber == accountNumber);

If account numbers are supposed to be unique, Single() can expose data integrity problems.

Quick comparison

MethodNo matchMultiple matches
First()ExceptionReturns first
FirstOrDefault()Default valueReturns first
Single()ExceptionException
SingleOrDefault()Default valueException

Choose the method based on the business rule, not personal preference.


6. Forgetting That LINQ Uses Deferred Execution

This is one of the most important LINQ concepts.

Consider:

var numbers = new List<int> { 1, 2, 3 };

var query = numbers.Where(n => n > 1);

numbers.Add(4);

foreach (var number in query)
{
    Console.WriteLine(number);
}

What gets printed?

2
3
4

Why?

Because Where() doesn't immediately execute the query.

The query is evaluated when you enumerate it.

For example:

foreach

causes execution.

So does:

ToList()
ToArray()
Count()
First()

and many other terminal operations.

If you want a snapshot

Use:

var query = numbers
    .Where(n => n > 1)
    .ToList();

Now the results are materialized immediately.


7. Executing Database Queries Inside a Loop

This is a particularly dangerous mistake with Entity Framework Core.

Imagine:

foreach (var order in orders)
{
    var customer = db.Customers
        .FirstOrDefault(c => c.Id == order.CustomerId);

    // ...
}

If there are 1,000 orders, this could result in hundreds or thousands of database queries.

This is commonly known as the N+1 query problem.

Better approach

If you need customer information, consider loading the relationship using a suitable query:

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

Then:

foreach (var order in orders)
{
    Console.WriteLine(order.Customer.Name);
}

Or project exactly what you need:

var orders = db.Orders
    .Select(o => new
    {
        o.Id,
        CustomerName = o.Customer.Name
    })
    .ToList();

Projection is often preferable when you don't need the complete entity graph.


8. Using Where(...).First() Instead of First(...)

You may see code like this:

var user = users
    .Where(u => u.Id == userId)
    .FirstOrDefault();

This works.

But it is unnecessarily verbose.

You can write:

var user = users
    .FirstOrDefault(u => u.Id == userId);

Likewise:

users
    .Where(u => u.IsActive)
    .Any();

can become:

users.Any(u => u.IsActive);

And:

users
    .Where(u => u.IsActive)
    .Count();

can become:

users.Count(u => u.IsActive);

Why does this matter?

It isn't usually a major performance difference.

It's primarily about clarity and intent.

Compare:

users.Where(u => u.IsActive).Any()

with:

users.Any(u => u.IsActive)

The second version immediately communicates:

"Does any active user exist?"

Clean LINQ is easier to maintain.


9. Calling ToList() Just Because "LINQ Needs It"

Another common pattern is:

var users = db.Users
    .Where(u => u.IsActive)
    .ToList();

users = users
    .Where(u => u.Age >= 18)
    .ToList();

The second filtering operation now happens in application memory.

If both conditions can be handled by the database, combine them:

var users = db.Users
    .Where(u => u.IsActive && u.Age >= 18)
    .ToList();

This allows the database to perform the filtering.

But ToList() isn't bad

ToList() is useful when you intentionally want to:

  • execute the query

  • create a snapshot

  • switch from database querying to LINQ-to-Objects

  • reuse the results multiple times

  • disconnect further processing from the database

The mistake is using it without understanding what it does.


10. Writing LINQ That Is Clever but Impossible to Read

LINQ allows you to write extremely compact code.

That's both a strength and a weakness.

For example:

var result = users
    .Where(u => u.IsActive)
    .GroupBy(u => u.Department)
    .Select(g => new
    {
        Department = g.Key,
        Count = g.Count(),
        Users = g.OrderByDescending(u => u.Experience)
                 .Take(3)
                 .Select(u => u.Name)
    })
    .OrderByDescending(x => x.Count)
    .ToList();

This is valid.

But imagine adding five more transformations.

Eventually, a single LINQ expression can become difficult to understand and debug.

Sometimes breaking the operation into meaningful steps is better:

var activeUsers = users
    .Where(u => u.IsActive);

var departmentGroups = activeUsers
    .GroupBy(u => u.Department);

var results = departmentGroups
    .Select(g => new DepartmentSummary
    {
        Department = g.Key,
        UserCount = g.Count()
    })
    .ToList();

Don't optimize for the fewest lines of code.

Optimize for:

readability + correctness + appropriate execution behavior.


Bonus: A Common Select Mistake

Here's another mistake worth knowing.

Suppose you have:

var users = new List<User>
{
    new() { Name = "John" },
    new() { Name = "Sarah" }
};

You want the names.

You might write:

var names = users.Select(u => new
{
    u.Name
});

This produces anonymous objects.

If you only need strings:

var names = users
    .Select(u => u.Name)
    .ToList();

The distinction becomes especially important when projecting data from Entity Framework Core.

Only select the columns you actually need.


LINQ Performance Checklist

Before shipping LINQ-heavy code, ask yourself:

1. Am I materializing too early?

Look for:

ToList()
ToArray()
ToDictionary()

2. Am I executing the same query repeatedly?

Watch for:

foreach
Count()
Any()
First()

on an unmaterialized query.

3. Do I need Any() instead of Count() > 0?

If you only need existence:

Any()

is usually the clearer choice.

4. Could this cause an N+1 query?

Be especially careful with database access inside loops.

5. Am I using the correct terminal operator?

Choose between:

First
FirstOrDefault
Single
SingleOrDefault

based on your business rule.

6. Am I accidentally switching from database LINQ to in-memory LINQ?

This often happens after:

ToList()

or:

AsEnumerable()

7. Is this LINQ readable?

If you need five minutes to understand your own query, simplify it.


Final Thoughts

LINQ isn't just about writing shorter C# code.

The most important thing is understanding when and where your query executes.

A few principles will prevent many LINQ problems:

Don't materialize too early
Don't enumerate expensive queries repeatedly
Use Any() for existence checks
Choose First/Single intentionally
Understand deferred execution
Avoid database queries inside loops
Keep LINQ readable

Once you understand these concepts, LINQ becomes much more than a convenient syntax for filtering collections.

It becomes a powerful tool for writing clean, efficient, and maintainable .NET applications.

And that's the real goal—not writing the shortest LINQ query, but writing the right query for the job.

Comments

Popular posts from this blog

How to Install .NET 10 SDK on Windows, Linux, and macOS

Microsoft Agent Framework Explained: A .NET Developer’s Architecture Guide

Why Is Your ASP.NET Core API Using So Much Memory? 7 Causes and Fixes