C# LINQ Explained: Query and Transform Collections Efficiently
LINQ, or Language Integrated Query, provides a consistent way to search, filter, sort, transform, group, and analyze data in C#. It works with collections and many other data sources while keeping query logic concise and readable.
What is LINQ?
LINQ is a set of language and library features that allows developers to query data using C# syntax. Common LINQ operations include Where, Select, OrderBy, GroupBy, Any, Count, and Sum.
List<int> numbers = new() { 10, 15, 20, 25, 30 };
var result = numbers.Where(number => number > 20);
foreach (int number in result)
{
Console.WriteLine(number);
}
Why Use LINQ?
- Reduce repetitive collection-processing code
- Make data operations easier to read
- Filter and transform collections efficiently
- Combine multiple operations into a pipeline
- Work with objects using strongly typed expressions
- Query different kinds of data sources with familiar patterns
The Where Method
Where filters a sequence according to a condition and returns the elements that satisfy that condition.
List<int> numbers = new() { 5, 10, 15, 20, 25 };
var result = numbers.Where(number => number >= 15);
foreach (int number in result)
{
Console.WriteLine(number);
}
The Select Method
Select transforms each element into another value. It is commonly used to project objects into simpler data structures.
List<int> numbers = new() { 1, 2, 3, 4 };
var squares = numbers.Select(number => number * number);
foreach (int square in squares)
{
Console.WriteLine(square);
}
Filtering and Projection Together
LINQ methods can be chained together to create readable data-processing pipelines.
List<int> numbers = new() { 5, 10, 15, 20, 25, 30 };
var result = numbers
.Where(number => number > 10)
.Select(number => number * 2);
foreach (int number in result)
{
Console.WriteLine(number);
}
Sorting with OrderBy
OrderBy sorts elements in ascending order. OrderByDescending can be used for descending order.
List<int> numbers = new() { 40, 10, 30, 20 };
var ascending = numbers.OrderBy(number => number);
var descending = numbers.OrderByDescending(number => number);
foreach (int number in ascending)
{
Console.WriteLine(number);
}
ThenBy for Secondary Sorting
ThenBy and ThenByDescending provide additional sorting rules when two or more elements have the same primary sort value.
var users = new List<User>
{
new User("Alice", "Developer"),
new User("Bob", "Admin"),
new User("Charlie", "Developer")
};
var result = users
.OrderBy(user => user.Role)
.ThenBy(user => user.Name);
foreach (var user in result)
{
Console.WriteLine($"{user.Role}: {user.Name}");
}
class User
{
public string Name { get; }
public string Role { get; }
public User(string name, string role)
{
Name = name;
Role = role;
}
}
First and FirstOrDefault
First returns the first matching element and throws when no element exists. FirstOrDefault returns the default value when the sequence contains no matching element.
List<int> numbers = new() { 10, 20, 30 };
int first = numbers.First();
int? value = numbers
.Where(number => number > 100)
.Cast<int?>()
.FirstOrDefault();
Console.WriteLine(first);
Console.WriteLine(value);
Any and All
Any checks whether at least one element satisfies a condition. All checks whether every element satisfies a condition.
List<int> numbers = new() { 10, 20, 30 };
bool hasLargeNumber = numbers.Any(number => number > 25);
bool allPositive = numbers.All(number => number > 0);
Console.WriteLine(hasLargeNumber);
Console.WriteLine(allPositive);
Count and Sum
LINQ provides aggregation methods such as Count, Sum, Average, Min, and Max for calculating values from a sequence.
List<int> numbers = new() { 10, 20, 30, 40 };
int count = numbers.Count();
int sum = numbers.Sum();
double average = numbers.Average();
int minimum = numbers.Min();
int maximum = numbers.Max();
Console.WriteLine(count);
Console.WriteLine(sum);
Console.WriteLine(average);
Console.WriteLine(minimum);
Console.WriteLine(maximum);
Grouping with GroupBy
GroupBy organizes elements into groups based on a selected key. It is useful for categories, departments, locations, and other grouped data.
var employees = new List<Employee>
{
new Employee("Alice", "IT"),
new Employee("Bob", "HR"),
new Employee("Charlie", "IT"),
new Employee("David", "HR")
};
var groups = employees.GroupBy(employee => employee.Department);
foreach (var group in groups)
{
Console.WriteLine(group.Key);
foreach (var employee in group)
{
Console.WriteLine(employee.Name);
}
}
class Employee
{
public string Name { get; }
public string Department { get; }
public Employee(string name, string department)
{
Name = name;
Department = department;
}
}
Distinct Values
Distinct removes duplicate values from a sequence.
List<string> categories = new()
{
"Books",
"Games",
"Books",
"Music",
"Games"
};
var uniqueCategories = categories.Distinct();
foreach (string category in uniqueCategories)
{
Console.WriteLine(category);
}
SelectMany
SelectMany combines nested collections into a single sequence. It is useful when each object contains its own collection of values.
var teams = new List<Team>
{
new Team("Team A", new List<string> { "Alice", "Bob" }),
new Team("Team B", new List<string> { "Charlie", "David" })
};
var members = teams.SelectMany(team => team.Members);
foreach (string member in members)
{
Console.WriteLine(member);
}
class Team
{
public string Name { get; }
public List<string> Members { get; }
public Team(string name, List<string> members)
{
Name = name;
Members = members;
}
}
LINQ Query Syntax
LINQ can also be written using query syntax. Method syntax is more common in many modern C# codebases, but query syntax can be useful for certain queries.
List<int> numbers = new() { 1, 2, 3, 4, 5, 6 };
var result = from number in numbers
where number > 3
select number;
foreach (int number in result)
{
Console.WriteLine(number);
}
Method Syntax vs Query Syntax
| Feature | Method Syntax | Query Syntax |
|---|---|---|
| Style | Extension methods | SQL-like syntax |
| Common Operators | Where, Select, OrderBy | where, select, orderby |
| Readability | Concise for pipelines | Useful for complex query expressions |
| Common Usage | Very common in modern C# | Useful for certain query scenarios |
Deferred Execution
Many LINQ queries use deferred execution, meaning the query is not executed until its results are enumerated. This can be useful, but developers should understand when the underlying data is actually accessed.
List<int> numbers = new() { 1, 2, 3 };
var query = numbers.Where(number => number > 1);
numbers.Add(4);
foreach (int number in query)
{
Console.WriteLine(number);
}
ToList and Immediate Execution
Methods such as ToList and ToArray force a LINQ query to execute immediately and store the resulting values.
var result = numbers
.Where(number => number > 10)
.ToList();
Console.WriteLine(result.Count);
LINQ with Objects
LINQ to Objects allows developers to query in-memory collections such as List
var products = new List<Product>
{
new Product("Laptop", 75000),
new Product("Mouse", 1500),
new Product("Monitor", 12000)
};
var expensiveProducts = products
.Where(product => product.Price > 10000)
.OrderBy(product => product.Price);
foreach (var product in expensiveProducts)
{
Console.WriteLine(product.Name);
}
class Product
{
public string Name { get; }
public decimal Price { get; }
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}
Common LINQ Mistakes
- Enumerating the same expensive query repeatedly
- Using First when no matching element is guaranteed
- Forgetting that many LINQ operations use deferred execution
- Calling ToList too early and creating unnecessary collections
- Writing overly complex LINQ chains that are difficult to maintain
- Using LINQ when a simple loop would be clearer or more efficient
LINQ Best Practices
- Keep query pipelines readable
- Use meaningful variable names
- Materialize results only when necessary
- Use Any when checking whether matching elements exist
- Choose FirstOrDefault when an element may not exist
- Avoid repeated enumeration of expensive sequences
- Prefer clarity over overly clever one-line queries
Real-World Applications
- Filtering products
- Searching user records
- Sorting application data
- Generating reports
- Grouping sales information
- Transforming API responses
- Processing collections in business applications
Practice Exercises
- Filter a list of numbers using Where
- Transform values using Select
- Sort objects with OrderBy
- Group employees by department
- Calculate totals using Sum and Average
- Remove duplicates using Distinct
- Flatten nested collections using SelectMany
- Build a complete LINQ query pipeline
Conclusion
LINQ provides a powerful and readable way to work with data in C#. By combining filtering, projection, sorting, grouping, aggregation, and other operations, developers can process collections with less repetitive code while keeping data-processing logic expressive.