C# LINQ Operators
Introduction
Language Integrated Query (LINQ) is a powerful feature in C# that allows you to query and manipulate data from different sources using a consistent syntax. LINQ operators are the building blocks of LINQ queries - they're methods that perform specific operations on sequences of data.
In this tutorial, we'll explore the various categories of LINQ operators, understand their purpose, and learn how to use them effectively in your C# applications. Whether you're working with collections, databases, XML, or other data sources, these operators will help you write cleaner, more readable code.
LINQ Operator Categories
LINQ operators can be grouped into the following main categories:
- Filtering operators
- Projection operators
- Sorting operators
- Grouping operators
- Join operators
- Set operators
- Aggregation operators
- Conversion operators
- Element operators
- Generation operators
Let's explore each category with practical examples.
Filtering Operators
Filtering operators allow you to select elements from a collection based on specified conditions.
1. Where
The Where
operator filters a sequence based on a predicate function.
// Filter numbers greater than 5
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var greaterThanFive = numbers.Where(n => n > 5);
// Output: 6, 7, 8, 9, 10
Console.WriteLine(string.Join(", ", greaterThanFive));
2. OfType
The OfType
operator filters elements based on their ability to be cast to a specified type.
// Filter only strings from a collection of objects
object[] mixedArray = { "Hello", 42, "World", 3.14, true };
var onlyStrings = mixedArray.OfType<string>();
// Output: Hello, World
Console.WriteLine(string.Join(", ", onlyStrings));
Projection Operators
Projection operators transform elements from a source sequence into a new form.
1. Select
The Select
operator projects each element of a sequence into a new form.
// Transform a list of numbers into their squares
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var squares = numbers.Select(n => n * n);
// Output: 1, 4, 9, 16, 25
Console.WriteLine(string.Join(", ", squares));
2. SelectMany
The SelectMany
operator flattens a sequence of sequences into a single sequence.
// Flatten a list of lists
List<List<int>> listOfLists = new List<List<int>>
{
new List<int> { 1, 2 },
new List<int> { 3, 4 },
new List<int> { 5, 6 }
};
var flattened = listOfLists.SelectMany(list => list);
// Output: 1, 2, 3, 4, 5, 6
Console.WriteLine(string.Join(", ", flattened));
Sorting Operators
Sorting operators organize the elements in a sequence based on specified criteria.
1. OrderBy and OrderByDescending
These operators sort elements in ascending or descending order.
List<string> fruits = new List<string> { "apple", "banana", "cherry", "date", "fig" };
// Ascending order
var ascending = fruits.OrderBy(fruit => fruit);
// Output: apple, banana, cherry, date, fig
Console.WriteLine(string.Join(", ", ascending));
// Descending order
var descending = fruits.OrderByDescending(fruit => fruit);
// Output: fig, date, cherry, banana, apple
Console.WriteLine(string.Join(", ", descending));
2. ThenBy and ThenByDescending
These operators perform secondary sorting.
// A list of students with name and score
var students = new List<(string Name, int Score)>
{
("Alice", 85),
("Bob", 90),
("Alice", 95),
("Charlie", 90)
};
// Sort by score descending, then by name ascending
var sortedStudents = students
.OrderByDescending(s => s.Score)
.ThenBy(s => s.Name);
// Output:
// Alice, 95
// Bob, 90
// Charlie, 90
// Alice, 85
foreach (var student in sortedStudents)
{
Console.WriteLine($"{student.Name}, {student.Score}");
}
3. Reverse
The Reverse
operator reverses the order of elements in a sequence.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var reversed = numbers.Reverse();
// Output: 5, 4, 3, 2, 1
Console.WriteLine(string.Join(", ", reversed));
Grouping Operators
Grouping operators organize elements of a sequence into groups based on a key.
1. GroupBy
The GroupBy
operator groups elements that share a common attribute.
List<string> words = new List<string> { "apple", "banana", "cherry", "almond", "blueberry" };
// Group words by their first letter
var groupedByFirstLetter = words.GroupBy(word => word[0]);
foreach (var group in groupedByFirstLetter)
{
Console.WriteLine($"Words starting with '{group.Key}': {string.Join(", ", group)}");
}
// Output:
// Words starting with 'a': apple, almond
// Words starting with 'b': banana, blueberry
// Words starting with 'c': cherry
2. ToLookup
Similar to GroupBy
, but results are immediately executed and stored in memory.
List<string> words = new List<string> { "apple", "banana", "cherry", "almond", "blueberry" };
// Create a lookup based on the first letter
var lookup = words.ToLookup(word => word[0]);
// Accessing groups by key
Console.WriteLine($"Words starting with 'a': {string.Join(", ", lookup['a'])}");
Console.WriteLine($"Words starting with 'b': {string.Join(", ", lookup['b'])}");
// Output:
// Words starting with 'a': apple, almond
// Words starting with 'b': banana, blueberry
Join Operators
Join operators combine elements from two sequences based on matching keys.
1. Join
The Join
operator performs an inner join between two sequences.
// Lists of departments and employees
var departments = new List<(int Id, string Name)>
{
(1, "HR"),
(2, "IT"),
(3, "Finance")
};
var employees = new List<(string Name, int DeptId)>
{
("Alice", 1),
("Bob", 2),
("Charlie", 2),
("David", 1),
("Eve", 4) // No matching department
};
// Join employees with departments
var employeeDepartments = employees.Join(
departments,
emp => emp.DeptId,
dept => dept.Id,
(emp, dept) => new { EmployeeName = emp.Name, DepartmentName = dept.Name }
);
foreach (var item in employeeDepartments)
{
Console.WriteLine($"{item.EmployeeName} works in {item.DepartmentName}");
}
// Output:
// Alice works in HR
// Bob works in IT
// Charlie works in IT
// David works in HR
// Note: Eve is not in the result because there's no department with ID 4
2. GroupJoin
The GroupJoin
operator performs a grouped join between two sequences.
// Group join departments with employees
var departmentEmployees = departments.GroupJoin(
employees,
dept => dept.Id,
emp => emp.DeptId,
(dept, emps) => new
{
DepartmentName = dept.Name,
Employees = emps.Select(e => e.Name)
}
);
foreach (var dept in departmentEmployees)
{
Console.WriteLine($"{dept.DepartmentName}: {string.Join(", ", dept.Employees)}");
}
// Output:
// HR: Alice, David
// IT: Bob, Charlie
// Finance:
Set Operators
Set operators perform set operations on sequences.
1. Distinct
The Distinct
operator removes duplicate elements from a sequence.
List<int> numbers = new List<int> { 1, 2, 3, 2, 1, 4, 5, 4 };
var distinctNumbers = numbers.Distinct();
// Output: 1, 2, 3, 4, 5
Console.WriteLine(string.Join(", ", distinctNumbers));
2. Union
The Union
operator produces the set union of two sequences.
List<int> set1 = new List<int> { 1, 2, 3 };
List<int> set2 = new List<int> { 3, 4, 5 };
var union = set1.Union(set2);
// Output: 1, 2, 3, 4, 5
Console.WriteLine(string.Join(", ", union));
3. Intersect
The Intersect
operator produces the set intersection of two sequences.
List<int> set1 = new List<int> { 1, 2, 3 };
List<int> set2 = new List<int> { 3, 4, 5 };
var intersection = set1.Intersect(set2);
// Output: 3
Console.WriteLine(string.Join(", ", intersection));
4. Except
The Except
operator produces the set difference between two sequences.
List<int> set1 = new List<int> { 1, 2, 3 };
List<int> set2 = new List<int> { 3, 4, 5 };
var difference = set1.Except(set2);
// Output: 1, 2
Console.WriteLine(string.Join(", ", difference));
Aggregation Operators
Aggregation operators compute a single value from a sequence of values.
1. Count, Sum, Min, Max, Average
These operators compute aggregate values from a sequence.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int count = numbers.Count(); // 5
int sum = numbers.Sum(); // 15
int min = numbers.Min(); // 1
int max = numbers.Max(); // 5
double avg = numbers.Average(); // 3
Console.WriteLine($"Count: {count}, Sum: {sum}, Min: {min}, Max: {max}, Average: {avg}");
// Output: Count: 5, Sum: 15, Min: 1, Max: 5, Average: 3
2. Aggregate
The Aggregate
operator applies an accumulator function over a sequence.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// Compute the product of all numbers
int product = numbers.Aggregate((acc, current) => acc * current);
// Output: 120 (1*2*3*4*5)
Console.WriteLine($"Product: {product}");
// Aggregate with seed value
string concatenated = numbers.Aggregate(
"Numbers: ", // seed
(str, num) => str + num + " "
);
// Output: Numbers: 1 2 3 4 5
Console.WriteLine(concatenated);
Conversion Operators
Conversion operators convert sequences to various collection types.
IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// Convert to different collection types
List<int> list = numbers.ToList();
int[] array = numbers.ToArray();
Dictionary<int, string> dict = numbers.ToDictionary(n => n, n => $"Number {n}");
HashSet<int> hashSet = new HashSet<int>(numbers);
// Display the dictionary
foreach (var kvp in dict)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// Output:
// 1: Number 1
// 2: Number 2
// 3: Number 3
// 4: Number 4
// 5: Number 5
Element Operators
Element operators return a single element from a sequence.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// First element
int first = numbers.First(); // 1
// Last element
int last = numbers.Last(); // 5
// Element at index 2
int elementAt = numbers.ElementAt(2); // 3
// First element greater than 3 or default
int firstOrDefault = numbers.FirstOrDefault(n => n > 10); // 0 (default)
// Single element that satisfies condition
int single = numbers.Single(n => n == 3); // 3
Console.WriteLine($"First: {first}, Last: {last}, ElementAt(2): {elementAt}");
Console.WriteLine($"FirstOrDefault(>10): {firstOrDefault}, Single(==3): {single}");
Generation Operators
Generation operators create new sequences of values.
// Generate a range of numbers from 1 to 5
var range = Enumerable.Range(1, 5);
// Output: 1, 2, 3, 4, 5
Console.WriteLine(string.Join(", ", range));
// Repeat a value 3 times
var repeated = Enumerable.Repeat("Hello", 3);
// Output: Hello, Hello, Hello
Console.WriteLine(string.Join(", ", repeated));
// Empty sequence of integers
var empty = Enumerable.Empty<int>();
Console.WriteLine($"Empty sequence count: {empty.Count()}"); // 0
Real-World Example: Processing Student Data
Let's combine multiple LINQ operators to solve a more complex problem. We'll process student data to find top performers in each department.
// Student class definition
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public string Department { get; set; }
public int Score { get; set; }
}
// Create sample data
List<Student> students = new List<Student>
{
new Student { Id = 1, Name = "Alice", Department = "CS", Score = 95 },
new Student { Id = 2, Name = "Bob", Department = "CS", Score = 85 },
new Student { Id = 3, Name = "Charlie", Department = "Math", Score = 90 },
new Student { Id = 4, Name = "David", Department = "Math", Score = 92 },
new Student { Id = 5, Name = "Eve", Department = "CS", Score = 78 },
new Student { Id = 6, Name = "Frank", Department = "Physics", Score = 88 },
new Student { Id = 7, Name = "Grace", Department = "Physics", Score = 91 }
};
// Find top performers in each department
var topPerformers = students
.GroupBy(s => s.Department)
.Select(group => new
{
Department = group.Key,
TopStudents = group
.OrderByDescending(s => s.Score)
.Take(2)
.Select(s => new { s.Name, s.Score })
});
// Display the results
foreach (var dept in topPerformers)
{
Console.WriteLine($"Department: {dept.Department}");
foreach (var student in dept.TopStudents)
{
Console.WriteLine($" {student.Name}: {student.Score}");
}
}
// Output:
// Department: CS
// Alice: 95
// Bob: 85
// Department: Math
// David: 92
// Charlie: 90
// Department: Physics
// Grace: 91
// Frank: 88
Summary
LINQ operators provide a powerful and consistent way to query and transform data in C#. We've covered the main categories of operators:
- Filtering operators like
Where
andOfType
to select specific elements - Projection operators like
Select
andSelectMany
to transform data - Sorting operators like
OrderBy
andThenBy
to arrange data - Grouping operators like
GroupBy
to organize data into categories - Join operators to combine data from different sources
- Set operators for performing set operations
- Aggregation operators to compute summary values
- Conversion operators to convert to different collection types
- Element operators to retrieve specific items
- Generation operators to create new sequences
By mastering these operators, you can write more concise, readable, and maintainable code. LINQ allows you to focus on what you want to accomplish rather than how to accomplish it.
Exercises
- Exercise 1: Write a LINQ query to find all even numbers in a list and double their values.
- Exercise 2: Given a list of words, find all words that start with a vowel and are longer than 5 characters.
- Exercise 3: Create a LINQ query to group a list of names by their length and count how many names are in each group.
- Exercise 4: Implement a LINQ query to join two collections: Products and Categories, and display each product with its category name.
- Exercise 5: Using LINQ, find the average score of students in each grade level, and identify the grade level with the highest average score.
Additional Resources
- Microsoft Documentation on LINQ
- 101 LINQ Samples
- LINQ Cheat Sheet
- LINQPad - A tool for interactively testing LINQ queries
Happy coding with LINQ!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)