Skip to main content

.NET LINQ Operators

Introduction

Language Integrated Query (LINQ) is one of the most powerful features in .NET, providing a unified approach to query and manipulate data from different sources. LINQ operators are the building blocks of LINQ queries, allowing you to filter, sort, group, join, and transform data collections with concise and readable syntax.

In this tutorial, we'll explore the essential LINQ operators that every .NET developer should know. Whether you're working with arrays, lists, databases, or XML, understanding these operators will help you write more efficient and elegant code.

What are LINQ Operators?

LINQ operators are methods that form the LINQ pattern. They are broadly categorized into:

  1. Query Operators - Used for querying data sources
  2. Projection Operators - Used for transforming elements
  3. Sorting Operators - Used for ordering elements
  4. Filtering Operators - Used for filtering collections
  5. Grouping Operators - Used for grouping elements
  6. Join Operators - Used for combining data sources
  7. Set Operators - Used for set operations like union and intersection
  8. Conversion Operators - Used for converting LINQ results to other types

Let's dive into each category with examples.

Filtering Operators

Where

The Where operator filters a collection based on a predicate.

csharp
// Example: Find all even numbers in a list
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = numbers.Where(n => n % 2 == 0);

// Output: 2, 4, 6, 8, 10
Console.WriteLine(string.Join(", ", evenNumbers));

OfType

The OfType operator filters elements based on their type.

csharp
// Example: Filter objects by type
object[] items = { "string", 1, 2.5, true, "another string", 3 };
var onlyStrings = items.OfType<string>();

// Output: string, another string
Console.WriteLine(string.Join(", ", onlyStrings));

Projection Operators

Select

The Select operator transforms each element in a collection.

csharp
// Example: Square each number in a list
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(n => n * n);

// Output: 1, 4, 9, 16, 25
Console.WriteLine(string.Join(", ", squaredNumbers));

SelectMany

The SelectMany operator flattens nested collections into a single collection.

csharp
// Example: 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

OrderBy / OrderByDescending

These operators sort elements in ascending or descending order.

csharp
// Example: Sort names alphabetically
List<string> names = new List<string> { "Charlie", "Bob", "Alice", "Dave" };

var ascending = names.OrderBy(name => name);
// Output: Alice, Bob, Charlie, Dave
Console.WriteLine(string.Join(", ", ascending));

var descending = names.OrderByDescending(name => name);
// Output: Dave, Charlie, Bob, Alice
Console.WriteLine(string.Join(", ", descending));

ThenBy / ThenByDescending

These operators perform secondary sorting.

csharp
// Example: Sort students by grade then by name
var students = new[]
{
new { Name = "Alice", Grade = 90 },
new { Name = "Bob", Grade = 85 },
new { Name = "Charlie", Grade = 90 },
new { Name = "Dave", Grade = 85 }
};

var sortedStudents = students
.OrderByDescending(s => s.Grade)
.ThenBy(s => s.Name);

// Output:
// Alice - 90
// Charlie - 90
// Bob - 85
// Dave - 85
foreach (var student in sortedStudents)
{
Console.WriteLine($"{student.Name} - {student.Grade}");
}

Grouping Operators

GroupBy

The GroupBy operator groups elements by a specified key.

csharp
// Example: Group words by their first letter
List<string> words = new List<string>
{
"apple", "banana", "avocado", "blueberry", "cherry"
};

var groupedWords = words.GroupBy(word => word[0]);

// Output:
// Key: a - apple, avocado
// Key: b - banana, blueberry
// Key: c - cherry
foreach (var group in groupedWords)
{
Console.WriteLine($"Key: {group.Key} - {string.Join(", ", group)}");
}

Join Operators

Join

The Join operator combines two collections based on matching keys.

csharp
// Example: Join departments and employees
var departments = new[]
{
new { Id = 1, Name = "HR" },
new { Id = 2, Name = "Engineering" },
new { Id = 3, Name = "Sales" }
};

var employees = new[]
{
new { Name = "Alice", DeptId = 1 },
new { Name = "Bob", DeptId = 2 },
new { Name = "Charlie", DeptId = 2 },
new { Name = "Dave", DeptId = 3 }
};

var employeesByDept = employees.Join(
departments,
emp => emp.DeptId,
dept => dept.Id,
(emp, dept) => new { emp.Name, DepartmentName = dept.Name }
);

// Output:
// Alice works in HR
// Bob works in Engineering
// Charlie works in Engineering
// Dave works in Sales
foreach (var item in employeesByDept)
{
Console.WriteLine($"{item.Name} works in {item.DepartmentName}");
}

GroupJoin

The GroupJoin operator joins two collections and groups the results.

csharp
// Example: Group employees by department
var deptEmployees = departments.GroupJoin(
employees,
dept => dept.Id,
emp => emp.DeptId,
(dept, emps) => new
{
DepartmentName = dept.Name,
Employees = emps.Select(e => e.Name)
}
);

// Output:
// HR: Alice
// Engineering: Bob, Charlie
// Sales: Dave
foreach (var dept in deptEmployees)
{
Console.WriteLine($"{dept.DepartmentName}: {string.Join(", ", dept.Employees)}");
}

Set Operators

Distinct

The Distinct operator removes duplicate elements from a collection.

csharp
// Example: Get unique numbers
int[] numbers = { 1, 2, 3, 1, 2, 4, 5, 3 };
var uniqueNumbers = numbers.Distinct();

// Output: 1, 2, 3, 4, 5
Console.WriteLine(string.Join(", ", uniqueNumbers));

Union, Intersect, Except

These operators perform set operations between collections.

csharp
// Example: Set operations
int[] set1 = { 1, 2, 3, 4, 5 };
int[] set2 = { 3, 4, 5, 6, 7 };

var union = set1.Union(set2);
// Output: 1, 2, 3, 4, 5, 6, 7
Console.WriteLine("Union: " + string.Join(", ", union));

var intersection = set1.Intersect(set2);
// Output: 3, 4, 5
Console.WriteLine("Intersection: " + string.Join(", ", intersection));

var difference = set1.Except(set2);
// Output: 1, 2
Console.WriteLine("Difference (set1 - set2): " + string.Join(", ", difference));

Element Operators

First, FirstOrDefault, Single, SingleOrDefault

These operators retrieve specific elements from a collection.

csharp
// Example: Using element operators
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
List<int> emptyList = new List<int>();

// First returns the first element or throws an exception if sequence is empty
int first = numbers.First(); // 1

// FirstOrDefault returns the first element or default value if sequence is empty
int firstOrDefault = emptyList.FirstOrDefault(); // 0

// Single returns the only element or throws an exception if there isn't exactly one element
int single = numbers.Where(n => n == 3).Single(); // 3

// SingleOrDefault returns the only element or default value if sequence is empty
// Throws if there's more than one match
int singleOrDefault = numbers.Where(n => n > 10).SingleOrDefault(); // 0

Console.WriteLine($"First: {first}");
Console.WriteLine($"FirstOrDefault: {firstOrDefault}");
Console.WriteLine($"Single: {single}");
Console.WriteLine($"SingleOrDefault: {singleOrDefault}");

Aggregation Operators

Count, Sum, Min, Max, Average

These operators compute aggregate values from a collection.

csharp
// Example: Aggregate functions
int[] numbers = { 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.0

Console.WriteLine($"Count: {count}");
Console.WriteLine($"Sum: {sum}");
Console.WriteLine($"Min: {min}");
Console.WriteLine($"Max: {max}");
Console.WriteLine($"Average: {avg}");

Generation Operators

Range, Repeat, Empty

These operators generate collections with specific patterns.

csharp
// Example: Generate collections
var range = Enumerable.Range(1, 5);
// Output: 1, 2, 3, 4, 5
Console.WriteLine("Range: " + string.Join(", ", range));

var repeated = Enumerable.Repeat("Hello", 3);
// Output: Hello, Hello, Hello
Console.WriteLine("Repeat: " + string.Join(", ", repeated));

var empty = Enumerable.Empty<int>();
// Output: (empty)
Console.WriteLine("Empty count: " + empty.Count());

Real-World Example: Processing Orders

Let's look at a more comprehensive example that combines several LINQ operators to process order data, similar to what you might do in an e-commerce application.

csharp
// Define classes
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}

public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public DateTime OrderDate { get; set; }
public List<OrderItem> Items { get; set; }
}

public class OrderItem
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}

// Sample data
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "Alice" },
new Customer { Id = 2, Name = "Bob" },
new Customer { Id = 3, Name = "Charlie" }
};

var products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 1200, Category = "Electronics" },
new Product { Id = 2, Name = "Headphones", Price = 100, Category = "Electronics" },
new Product { Id = 3, Name = "Book", Price = 20, Category = "Books" },
new Product { Id = 4, Name = "Smartphone", Price = 800, Category = "Electronics" },
new Product { Id = 5, Name = "T-Shirt", Price = 25, Category = "Clothing" }
};

var orders = new List<Order>
{
new Order
{
Id = 1,
CustomerId = 1,
OrderDate = new DateTime(2023, 1, 15),
Items = new List<OrderItem>
{
new OrderItem { ProductId = 1, Quantity = 1 },
new OrderItem { ProductId = 2, Quantity = 1 }
}
},
new Order
{
Id = 2,
CustomerId = 2,
OrderDate = new DateTime(2023, 2, 10),
Items = new List<OrderItem>
{
new OrderItem { ProductId = 3, Quantity = 2 },
new OrderItem { ProductId = 5, Quantity = 3 }
}
},
new Order
{
Id = 3,
CustomerId = 1,
OrderDate = new DateTime(2023, 3, 20),
Items = new List<OrderItem>
{
new OrderItem { ProductId = 4, Quantity = 1 }
}
},
new Order
{
Id = 4,
CustomerId = 3,
OrderDate = new DateTime(2023, 3, 25),
Items = new List<OrderItem>
{
new OrderItem { ProductId = 1, Quantity = 1 },
new OrderItem { ProductId = 3, Quantity = 5 }
}
}
};

// LINQ queries

// 1. Get all orders with customer names
var ordersWithCustomers = orders
.Join(
customers,
o => o.CustomerId,
c => c.Id,
(order, customer) => new
{
OrderId = order.Id,
CustomerName = customer.Name,
OrderDate = order.OrderDate,
Items = order.Items
}
);

// 2. Calculate the total value of each order
var orderValues = orders.Select(order => new
{
OrderId = order.Id,
TotalValue = order.Items
.Join(
products,
item => item.ProductId,
product => product.Id,
(item, product) => item.Quantity * product.Price
)
.Sum()
});

// 3. Find customers who have spent over $1000
var highValueCustomers = customers
.Join(
orders,
c => c.Id,
o => o.CustomerId,
(customer, order) => new
{
Customer = customer,
Order = order
}
)
.SelectMany(
co => co.Order.Items,
(co, item) => new
{
co.Customer,
Item = item
}
)
.Join(
products,
ci => ci.Item.ProductId,
p => p.Id,
(ci, product) => new
{
ci.Customer,
Value = ci.Item.Quantity * product.Price
}
)
.GroupBy(
cv => cv.Customer.Id,
(customerId, customerValues) => new
{
CustomerId = customerId,
CustomerName = customerValues.First().Customer.Name,
TotalSpent = customerValues.Sum(cv => cv.Value)
}
)
.Where(c => c.TotalSpent > 1000);

// 4. Find the most popular product category
var popularCategories = orders
.SelectMany(o => o.Items)
.Join(
products,
item => item.ProductId,
p => p.Id,
(item, product) => new
{
Category = product.Category,
Quantity = item.Quantity
}
)
.GroupBy(
pc => pc.Category,
(category, items) => new
{
Category = category,
TotalQuantity = items.Sum(i => i.Quantity)
}
)
.OrderByDescending(c => c.TotalQuantity);

// Display results
Console.WriteLine("Orders with customer names:");
foreach (var order in ordersWithCustomers)
{
Console.WriteLine($"Order {order.OrderId} by {order.CustomerName} on {order.OrderDate.ToShortDateString()}");
}

Console.WriteLine("\nOrder values:");
foreach (var order in orderValues)
{
Console.WriteLine($"Order {order.OrderId}: ${order.TotalValue}");
}

Console.WriteLine("\nHigh-value customers (spent over $1000):");
foreach (var customer in highValueCustomers)
{
Console.WriteLine($"{customer.CustomerName}: ${customer.TotalSpent}");
}

Console.WriteLine("\nPopular categories:");
foreach (var category in popularCategories)
{
Console.WriteLine($"{category.Category}: {category.TotalQuantity} items");
}

Performance Considerations with LINQ Operators

While LINQ operators are powerful and convenient, there are some performance considerations you should keep in mind:

  1. Deferred Execution: Most LINQ queries use deferred execution, meaning they don't execute until you iterate over the results or call methods like ToList(), ToArray(), or aggregation operators.

  2. Multiple Enumerations: Be careful not to enumerate the same query multiple times, which can cause performance issues.

csharp
// Example: Inefficient multiple enumerations
var numbers = GetNumbers(); // Returns IEnumerable<int>
var count = numbers.Count(); // First enumeration
var sum = numbers.Sum(); // Second enumeration

// More efficient:
var numbersList = GetNumbers().ToList();
var count = numbersList.Count;
var sum = numbersList.Sum();
  1. Query Optimization: For large data sets, consider optimizing your queries by filtering early.
csharp
// Less efficient
var result = collection.Select(item => ExpensiveTransform(item))
.Where(item => item.SomeProperty > 100);

// More efficient - filter first, then transform
var result = collection.Where(item => item.SomeProperty > 100)
.Select(item => ExpensiveTransform(item));

Summary

LINQ operators provide a powerful and consistent way to query and manipulate collections in .NET. In this tutorial, we've covered:

  • Filtering collections with Where and OfType
  • Transforming data with Select and SelectMany
  • Sorting with OrderBy and related operators
  • Grouping with GroupBy
  • Joining collections with Join and GroupJoin
  • Set operations with Distinct, Union, Intersect, and Except
  • Retrieving elements with First, Single, and related operators
  • Aggregating data with Count, Sum, Min, Max, and Average
  • Generating collections with Range, Repeat, and Empty

LINQ operators not only make your code more concise and readable but also help you focus on what you want to achieve rather than how to do it. As you become more familiar with these operators, you'll find yourself writing more elegant and efficient code.

Additional Resources and Exercises

Resources

Exercises

  1. Beginner: Create a list of integers and use LINQ to find:

    • All even numbers
    • The sum of all numbers greater than 10
    • The largest number in the list
  2. Intermediate: Create a list of products with Name, Price, and Category. Use LINQ to:

    • Find the average price per category
    • Find the most expensive product in each category
    • Find categories that have more than 3 products
  3. Advanced: Create a small library management system with books, authors, and borrowers. Use LINQ to:

    • Find books borrowed by a specific user
    • Find authors who have written more than 3 books
    • Find the most popular book (borrowed most times)
    • Find users who have overdue books
  4. Challenge: Implement a simple movie recommendation system using LINQ by matching users with similar movie ratings.

By practicing these exercises, you'll gain confidence in using LINQ operators and discover how they can simplify your code while making it more expressive.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)