Skip to main content

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:

  1. Filtering operators
  2. Projection operators
  3. Sorting operators
  4. Grouping operators
  5. Join operators
  6. Set operators
  7. Aggregation operators
  8. Conversion operators
  9. Element operators
  10. 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.

csharp
// 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.

csharp
// 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.

csharp
// 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.

csharp
// 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.

csharp
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.

csharp
// 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.

csharp
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.

csharp
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.

csharp
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.

csharp
// 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.

csharp
// 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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
// 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.

csharp
// 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 and OfType to select specific elements
  • Projection operators like Select and SelectMany to transform data
  • Sorting operators like OrderBy and ThenBy 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

  1. Exercise 1: Write a LINQ query to find all even numbers in a list and double their values.
  2. Exercise 2: Given a list of words, find all words that start with a vowel and are longer than 5 characters.
  3. Exercise 3: Create a LINQ query to group a list of names by their length and count how many names are in each group.
  4. Exercise 4: Implement a LINQ query to join two collections: Products and Categories, and display each product with its category name.
  5. 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

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! :)