Lesson 128 min read

LINQ

Query your data like a search engine — right inside C#

What Is LINQ?

LINQ stands for Language Integrated Query. It lets you search, filter, sort, and transform collections using clean, readable syntax — kind of like writing SQL database queries, but for any collection in C#.

Imagine you have a list of 1,000 students. Without LINQ, you'd write loops, if-statements, and temporary variables. With LINQ, you write something like "give me all students over 18, sorted by GPA, and just their names." One line. Done.

Understanding time complexity helps you choose the right LINQ methods for large datasets. LINQ has two styles:

  • Method syntax: list.Where(...).Select(...) — more common, uses lambda arrows
  • Query syntax: from x in list where ... select ... — looks like SQL

Both produce the same result. Most C# developers prefer method syntax, but query syntax is great for complex joins.

LINQ Basics — Where, Select, OrderBy

var students = new List<(string Name, int Age, double GPA)>
{
("Alice", 20, 3.8),
("Bob", 17, 3.2),
("Charlie", 22, 3.9),
("Diana", 19, 2.7),
("Eve", 21, 3.5)
};
// Method syntax — filter, sort, transform
var honorRoll = students
.Where(s => s.GPA >= 3.5) // filter: GPA 3.5+
.OrderByDescending(s => s.GPA) // sort: highest first
.Select(s => $"{s.Name} ({s.GPA})"); // transform: just name + GPA
Console.WriteLine("Honor Roll (method syntax):");
foreach (var entry in honorRoll)
Console.WriteLine($" {entry}");
// Query syntax — same result, SQL-like style
var honorRoll2 = from s in students
where s.GPA >= 3.5
orderby s.GPA descending
select $"{s.Name} ({s.GPA})";
Console.WriteLine("\nHonor Roll (query syntax):");
foreach (var entry in honorRoll2)
Console.WriteLine($" {entry}");
Output
Honor Roll (method syntax):
  Charlie (3.9)
  Alice (3.8)
  Eve (3.5)

Honor Roll (query syntax):
  Charlie (3.9)
  Alice (3.8)
  Eve (3.5)

Powerful LINQ Methods

LINQ has dozens of methods. Here are the ones you'll use constantly:

  • .Where() — filter items that match a condition
  • .Select() — transform each item into something else
  • .OrderBy() / .OrderByDescending() — sort
  • .GroupBy() — group items by a key
  • .First() / .FirstOrDefault() — get the first matching item
  • .Any() — is there AT LEAST ONE match? (returns bool)
  • .All() — do ALL items match? (returns bool)
  • .Count() — how many items match?
  • .Sum() / .Average() / .Min() / .Max() — math on collections
  • .Distinct() — remove duplicates
  • .Take(n) / .Skip(n) — pagination

Any, All, First, Count, GroupBy

var products = new List<(string Name, string Category, decimal Price)>
{
("Laptop", "Electronics", 999.99m),
("Mouse", "Electronics", 29.99m),
("Desk", "Furniture", 249.99m),
("Chair", "Furniture", 399.99m),
("Headphones", "Electronics", 79.99m),
("Lamp", "Furniture", 49.99m)
};
// Any — is there anything expensive?
bool hasExpensive = products.Any(p => p.Price > 500);
Console.WriteLine($"Has expensive items: {hasExpensive}");
// All — is everything under $100?
bool allCheap = products.All(p => p.Price < 100);
Console.WriteLine($"All items under $100: {allCheap}");
// First — get the cheapest item
var cheapest = products.OrderBy(p => p.Price).First();
Console.WriteLine($"Cheapest: {cheapest.Name} at {cheapest.Price:C}");
// Count & Sum
int electronicCount = products.Count(p => p.Category == "Electronics");
decimal totalValue = products.Sum(p => p.Price);
Console.WriteLine($"Electronics: {electronicCount} items");
Console.WriteLine($"Total value: {totalValue:C}");
// GroupBy — group products by category
var groups = products.GroupBy(p => p.Category);
Console.WriteLine("\nBy category:");
foreach (var group in groups)
{
Console.WriteLine($" {group.Key}: {group.Count()} items, avg {group.Average(p => p.Price):C}");
}
Output
Has expensive items: True
All items under $100: False
Cheapest: Mouse at $29.99
Electronics: 3 items
Total value: $1,809.94

By category:
  Electronics: 3 items, avg $369.99
  Furniture: 3 items, avg $233.32

Chaining, Take/Skip, and Distinct

var numbers = Enumerable.Range(1, 20).ToList(); // [1, 2, 3, ... 20]
// Chain multiple operations
var result = numbers
.Where(n => n % 2 == 0) // evens: 2, 4, 6, 8...
.Select(n => n * n) // squared: 4, 16, 36, 64...
.Where(n => n > 20) // filter: 36, 64, 100...
.Take(3); // first 3 only
Console.WriteLine($"Chained: [{string.Join(", ", result)}]");
// Pagination with Skip and Take
var page1 = numbers.Skip(0).Take(5);
var page2 = numbers.Skip(5).Take(5);
var page3 = numbers.Skip(10).Take(5);
Console.WriteLine($"Page 1: [{string.Join(", ", page1)}]");
Console.WriteLine($"Page 2: [{string.Join(", ", page2)}]");
Console.WriteLine($"Page 3: [{string.Join(", ", page3)}]");
// Distinct — remove duplicates
var tags = new[] { "csharp", "dotnet", "csharp", "linq", "dotnet", "linq" };
var unique = tags.Distinct().OrderBy(t => t);
Console.WriteLine($"Unique tags: [{string.Join(", ", unique)}]");
// FirstOrDefault — safe first (returns default if empty)
int? firstBig = numbers.Where(n => n > 100).FirstOrDefault();
Console.WriteLine($"First > 100: {firstBig}"); // 0 (default for int)
Output
Chained: [36, 64, 100]
Page 1: [1, 2, 3, 4, 5]
Page 2: [6, 7, 8, 9, 10]
Page 3: [11, 12, 13, 14, 15]
Unique tags: [csharp, dotnet, linq]
First > 100: 0
Note: ⚡ LINQ is lazy! Methods like .Where() and .Select() don't actually run until you iterate (foreach) or force evaluation (.ToList(), .Count(), .First()). This means LINQ only processes what it needs — if you Take(3), it stops after finding 3 matches, even from a million items.

Quick check

What's the difference between .First() and .FirstOrDefault()?
Collections & GenericsException Handling