LINQ Fundamentals — Querying Collections with Method Syntax

LINQ (Language Integrated Query) is a set of extension methods on IEnumerable<T> and IQueryable<T> that bring SQL-style querying directly into C# code. Instead of writing loops to filter, sort, and transform collections, you chain method calls that express your intent clearly and concisely. LINQ is used everywhere in .NET: filtering EF Core database results, transforming DTOs in service layers, grouping and aggregating API response data, and building complex business logic from simple composable operations.

Where — Filtering

var posts = new List<Post>
{
    new Post { Id = 1, Title = "Intro to C#",   IsPublished = true,  ViewCount = 1200 },
    new Post { Id = 2, Title = "LINQ Guide",    IsPublished = true,  ViewCount = 850  },
    new Post { Id = 3, Title = "Draft Post",    IsPublished = false, ViewCount = 0    },
    new Post { Id = 4, Title = "EF Core Deep",  IsPublished = true,  ViewCount = 3500 },
};

// Where — returns all items matching the predicate
var published = posts.Where(p => p.IsPublished);
// { Id=1, Id=2, Id=4 }

// Chain multiple Where clauses (both conditions must be true)
var popularPublished = posts
    .Where(p => p.IsPublished)
    .Where(p => p.ViewCount > 1000);
// { Id=1, Id=4 }

// Or combine with &&
var same = posts.Where(p => p.IsPublished && p.ViewCount > 1000);

Select — Projection

// Select — transform each element into a new shape
var titles = posts.Select(p => p.Title);
// ["Intro to C#", "LINQ Guide", "Draft Post", "EF Core Deep"]

// Project to an anonymous type
var summaries = posts.Select(p => new { p.Id, p.Title, p.IsPublished });

// Project to a DTO (most common in ASP.NET Core)
var dtos = posts
    .Where(p => p.IsPublished)
    .Select(p => new PostDto
    {
        Id    = p.Id,
        Title = p.Title,
        Views = p.ViewCount,
    })
    .ToList();

// Select with index
var indexed = posts.Select((p, i) => $"{i + 1}. {p.Title}");
// ["1. Intro to C#", "2. LINQ Guide", ...]
Note: Deferred execution means a LINQ query is not actually run when you write it — it runs when you iterate the result (with foreach, ToList(), Count(), etc.). var query = posts.Where(p => p.IsPublished) creates a query object but performs zero work. query.ToList() executes it. This matters because: (1) you can compose queries by adding more operators before materialising, and (2) if the source changes between defining and iterating the query, the query sees the updated data. Always materialise with .ToList() when you need a snapshot.
Tip: Chain LINQ operators to build expressive, readable queries. The method chain reads like a pipeline of transformations: start with the source, filter, sort, project, take. Each operator is small and focused. This composability is far more maintainable than equivalent nested loops and temporary variables. In ASP.NET Core service methods, a well-named LINQ chain is often self-documenting: posts.Where(IsPublished).OrderByDescending(ByViews).Take(pageSize).Select(ToDto).
Warning: Do not call .ToList() prematurely on an EF Core query if you intend to add more filtering operators. Calling ToList() materialises the entire result set into memory, and any subsequent Where runs in C# (client-side), not in SQL. Build the complete query first, then materialise once at the end. The rule: add all Where, OrderBy, Skip, Take, and Select operators before calling ToListAsync().

Ordering and Single-Element Retrieval

// OrderBy / OrderByDescending — sort by a key
var byViews = posts.OrderByDescending(p => p.ViewCount);
// { Id=4(3500), Id=1(1200), Id=2(850), Id=3(0) }

// ThenBy — secondary sort
var sorted = posts
    .OrderBy(p => p.IsPublished)
    .ThenByDescending(p => p.ViewCount);

// Single-element retrieval
Post? first   = posts.FirstOrDefault();                         // first or null
Post? byId    = posts.FirstOrDefault(p => p.Id == 2);          // first matching or null
Post? single  = posts.SingleOrDefault(p => p.Id == 1);         // throws if >1 match
Post  orThrow = posts.First(p => p.IsPublished);               // throws if none

// Last
Post? last    = posts.LastOrDefault(p => p.IsPublished);       // last matching or null

Aggregation Basics

int   total      = posts.Count();                              // 4
int   published  = posts.Count(p => p.IsPublished);           // 3
bool  hasAny     = posts.Any();                                // true
bool  hasPopular = posts.Any(p => p.ViewCount > 2000);        // true
bool  allPubl    = posts.All(p => p.IsPublished);             // false
long  totalViews = posts.Sum(p => (long)p.ViewCount);         // 5550
int   maxViews   = posts.Max(p => p.ViewCount);               // 3500
double avgViews  = posts.Average(p => p.ViewCount);           // 1387.5

Common Mistakes

Mistake 1 — Using First() instead of FirstOrDefault() when element may be absent

❌ Wrong — throws InvalidOperationException if no match:

Post post = posts.First(p => p.Id == 999);   // throws!

✅ Correct:

Post? post = posts.FirstOrDefault(p => p.Id == 999);   // null if not found
if (post is not null) { /* use it */ }

Mistake 2 — Materialising too early with ToList() before building the full query

❌ Wrong — loads all posts into memory, then filters in C#:

var all    = await _db.Posts.ToListAsync();     // loads everything first
var result = all.Where(p => p.IsPublished);    // filtered in-memory

✅ Correct — build the complete query, materialise once:

var result = await _db.Posts.Where(p => p.IsPublished).ToListAsync();

🧠 Test Yourself

You write var query = posts.Where(p => p.IsPublished); and later add more items to posts. When you call query.ToList(), does it include the newly added items?