Skip to main content

C# LINQ with Collections

Introduction

Language Integrated Query (LINQ) is one of the most powerful features in C#, allowing you to write expressive, declarative code to query and manipulate collections. Instead of writing complex loops with conditional logic, LINQ provides a clean, SQL-like syntax to filter, sort, group, join, and transform data in your collections.

In this tutorial, we'll explore how to use LINQ with various collection types in C# and how it can simplify your code while making it more readable and maintainable.

LINQ Basics

LINQ can be used with any collection that implements the IEnumerable<T> interface, including arrays, lists, dictionaries, and more. There are two main syntaxes for writing LINQ queries:

  1. Query Syntax: Similar to SQL, using keywords like from, where, select
  2. Method Syntax: Using extension methods like Where(), Select(), OrderBy()

Let's start with some basic examples:

Setting Up Our Collections

First, let's create some collections that we'll use throughout this tutorial:

csharp
// A simple integer array
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// A list of products
List<Product> products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 1200, Category = "Electronics" },
new Product { Id = 2, Name = "Desk Chair", Price = 250, Category = "Furniture" },
new Product { Id = 3, Name = "Smartphone", Price = 800, Category = "Electronics" },
new Product { Id = 4, Name = "Bookshelf", Price = 150, Category = "Furniture" },
new Product { Id = 5, Name = "Headphones", Price = 100, Category = "Electronics" },
new Product { Id = 6, Name = "Coffee Table", Price = 200, Category = "Furniture" }
};

// A simple dictionary
Dictionary<string, int> scores = new Dictionary<string, int>
{
{ "Alice", 95 },
{ "Bob", 87 },
{ "Charlie", 92 },
{ "Diana", 78 },
{ "Edward", 88 }
};

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

Basic LINQ Operations

Filtering with Where

Query Syntax:

csharp
// Get all even numbers
var evenNumbers = from num in numbers
where num % 2 == 0
select num;

Console.WriteLine("Even numbers:");
foreach (var num in evenNumbers)
{
Console.Write($"{num} ");
}
// Output: Even numbers: 2 4 6 8 10

Method Syntax:

csharp
// Get all even numbers
var evenNumbers = numbers.Where(num => num % 2 == 0);

Console.WriteLine("Even numbers:");
foreach (var num in evenNumbers)
{
Console.Write($"{num} ");
}
// Output: Even numbers: 2 4 6 8 10

Sorting with OrderBy and OrderByDescending

csharp
// Sort products by price (ascending)
var sortedProducts = products.OrderBy(p => p.Price);

Console.WriteLine("\nProducts sorted by price (ascending):");
foreach (var product in sortedProducts)
{
Console.WriteLine($"{product.Name}: ${product.Price}");
}
// Output:
// Products sorted by price (ascending):
// Headphones: $100
// Bookshelf: $150
// Coffee Table: $200
// Desk Chair: $250
// Smartphone: $800
// Laptop: $1200

// Sort products by price (descending)
var expensiveFirst = products.OrderByDescending(p => p.Price);

Console.WriteLine("\nProducts sorted by price (descending):");
foreach (var product in expensiveFirst)
{
Console.WriteLine($"{product.Name}: ${product.Price}");
}
// Output:
// Products sorted by price (descending):
// Laptop: $1200
// Smartphone: $800
// Desk Chair: $250
// Coffee Table: $200
// Bookshelf: $150
// Headphones: $100

Projection with Select

The Select operation allows you to transform the elements of a collection:

csharp
// Extract just the names of products
var productNames = products.Select(p => p.Name);

Console.WriteLine("\nProduct names:");
foreach (var name in productNames)
{
Console.WriteLine(name);
}
// Output:
// Product names:
// Laptop
// Desk Chair
// Smartphone
// Bookshelf
// Headphones
// Coffee Table

// Create anonymous objects with selected properties
var simplifiedProducts = products.Select(p => new { p.Name, p.Price });

Console.WriteLine("\nSimplified products:");
foreach (var item in simplifiedProducts)
{
Console.WriteLine($"{item.Name}: ${item.Price}");
}
// Output:
// Simplified products:
// Laptop: $1200
// Desk Chair: $250
// Smartphone: $800
// Bookshelf: $150
// Headphones: $100
// Coffee Table: $200

Combining LINQ Operations

LINQ operations can be chained together to create more complex queries:

csharp
// Get expensive electronics (price > 500) sorted by price
var expensiveElectronics = products
.Where(p => p.Category == "Electronics" && p.Price > 500)
.OrderBy(p => p.Price)
.Select(p => new { p.Name, p.Price });

Console.WriteLine("\nExpensive electronics:");
foreach (var item in expensiveElectronics)
{
Console.WriteLine($"{item.Name}: ${item.Price}");
}
// Output:
// Expensive electronics:
// Smartphone: $800
// Laptop: $1200

Quantification Operations

LINQ provides methods to check conditions across collections:

Any and All

csharp
// Check if any product costs more than $1000
bool hasExpensiveProduct = products.Any(p => p.Price > 1000);
Console.WriteLine($"\nHas product over $1000: {hasExpensiveProduct}");
// Output: Has product over $1000: True

// Check if all products cost more than $50
bool allProductsAbove50 = products.All(p => p.Price > 50);
Console.WriteLine($"All products above $50: {allProductsAbove50}");
// Output: All products above $50: True

Aggregation Operations

LINQ provides various aggregation methods:

csharp
// Count items
int electronicsCount = products.Count(p => p.Category == "Electronics");
Console.WriteLine($"\nNumber of electronics: {electronicsCount}");
// Output: Number of electronics: 3

// Sum values
decimal totalProductsValue = products.Sum(p => p.Price);
Console.WriteLine($"Total value of all products: ${totalProductsValue}");
// Output: Total value of all products: $2700

// Average
decimal averagePrice = products.Average(p => p.Price);
Console.WriteLine($"Average product price: ${averagePrice}");
// Output: Average product price: $450

// Min and Max
decimal cheapestPrice = products.Min(p => p.Price);
decimal mostExpensivePrice = products.Max(p => p.Price);
Console.WriteLine($"Price range: ${cheapestPrice} - ${mostExpensivePrice}");
// Output: Price range: $100 - $1200

Grouping with LINQ

The GroupBy operation allows you to group collection items by a key:

csharp
// Group products by category
var groupedByCategory = products.GroupBy(p => p.Category);

Console.WriteLine("\nProducts grouped by category:");
foreach (var group in groupedByCategory)
{
Console.WriteLine($"\n{group.Key} ({group.Count()} items):");
foreach (var product in group)
{
Console.WriteLine($" - {product.Name}: ${product.Price}");
}
}
// Output:
// Products grouped by category:
//
// Electronics (3 items):
// - Laptop: $1200
// - Smartphone: $800
// - Headphones: $100
//
// Furniture (3 items):
// - Desk Chair: $250
// - Bookshelf: $150
// - Coffee Table: $200

Working with Dictionaries

LINQ can also be used with dictionaries:

csharp
// Get students with scores >= 90
var highScorers = scores.Where(pair => pair.Value >= 90)
.Select(pair => pair.Key);

Console.WriteLine("\nStudents with high scores (>= 90):");
foreach (var student in highScorers)
{
Console.WriteLine(student);
}
// Output:
// Students with high scores (>= 90):
// Alice
// Charlie

Set Operations

LINQ provides set operations like Union, Intersect, and Except:

csharp
int[] set1 = { 1, 2, 3, 4, 5 };
int[] set2 = { 4, 5, 6, 7, 8 };

// Union: combine unique elements from both sets
var unionResult = set1.Union(set2);
Console.WriteLine("\nUnion: " + string.Join(", ", unionResult));
// Output: Union: 1, 2, 3, 4, 5, 6, 7, 8

// Intersect: elements that appear in both sets
var intersectResult = set1.Intersect(set2);
Console.WriteLine("Intersect: " + string.Join(", ", intersectResult));
// Output: Intersect: 4, 5

// Except: elements in first set but not in second
var exceptResult = set1.Except(set2);
Console.WriteLine("Except: " + string.Join(", ", exceptResult));
// Output: Except: 1, 2, 3

Real-world Example: E-commerce Product Filtering

Let's build a more practical example simulating product filtering in an e-commerce application:

csharp
// More expanded product list
List<Product> inventory = new List<Product>
{
new Product { Id = 1, Name = "Gaming Laptop", Price = 1200, Category = "Electronics" },
new Product { Id = 2, Name = "Office Chair", Price = 250, Category = "Furniture" },
new Product { Id = 3, Name = "Smartphone", Price = 800, Category = "Electronics" },
new Product { Id = 4, Name = "Bookshelf", Price = 150, Category = "Furniture" },
new Product { Id = 5, Name = "Wireless Headphones", Price = 180, Category = "Electronics" },
new Product { Id = 6, Name = "Coffee Table", Price = 200, Category = "Furniture" },
new Product { Id = 7, Name = "Bluetooth Speaker", Price = 120, Category = "Electronics" },
new Product { Id = 8, Name = "Sofa", Price = 850, Category = "Furniture" },
new Product { Id = 9, Name = "Tablet", Price = 350, Category = "Electronics" },
new Product { Id = 10, Name = "Dining Table", Price = 400, Category = "Furniture" }
};

// User's filter settings
string categoryFilter = "Electronics";
decimal minPrice = 150;
decimal maxPrice = 500;
string sortBy = "price";
bool ascending = true;

// Apply filters using LINQ
var filteredProducts = inventory
.Where(p => p.Category == categoryFilter)
.Where(p => p.Price >= minPrice && p.Price <= maxPrice);

// Apply sorting
IEnumerable<Product> sortedFilteredProducts;
if (sortBy.ToLower() == "price")
{
sortedFilteredProducts = ascending
? filteredProducts.OrderBy(p => p.Price)
: filteredProducts.OrderByDescending(p => p.Price);
}
else // Sort by name
{
sortedFilteredProducts = ascending
? filteredProducts.OrderBy(p => p.Name)
: filteredProducts.OrderByDescending(p => p.Name);
}

// Display results
Console.WriteLine($"\nFiltered {categoryFilter} products (${minPrice} - ${maxPrice}):");
foreach (var product in sortedFilteredProducts)
{
Console.WriteLine($"{product.Name}: ${product.Price}");
}
// Output:
// Filtered Electronics products ($150 - $500):
// Wireless Headphones: $180
// Tablet: $350

Deferred Execution

An important concept to understand with LINQ is that many LINQ operations use deferred execution:

csharp
// Create a query but don't execute it yet
var query = numbers.Where(n => n % 2 == 0);

// At this point, nothing has been evaluated

// Add a new number to the source collection
int[] moreNumbers = numbers.Append(11).ToArray();

// Now the query is executed, including the new number
Console.WriteLine("\nEven numbers after modification:");
foreach (var num in query)
{
Console.Write($"{num} ");
}
// Output: Even numbers after modification: 2 4 6 8 10

// You can force immediate execution with methods like ToList(), ToArray(), Count(), etc.
var immediateResults = numbers.Where(n => n % 2 == 0).ToList();

Performance Considerations

While LINQ is convenient and powerful, it's important to be aware of potential performance implications:

  1. Avoid multiple iterations over the same data
  2. Use methods like FirstOrDefault() instead of Where().FirstOrDefault() when appropriate
  3. Be mindful of deferred execution, especially with database contexts
  4. For large collections, consider using AsParallel() for parallel processing
csharp
// Inefficient - multiple iterations
var count = products.Where(p => p.Price > 200).Count();
var expensive = products.Where(p => p.Price > 200).ToList();

// More efficient - iterate once
var expensiveProducts = products.Where(p => p.Price > 200).ToList();
var expensiveCount = expensiveProducts.Count;

// For very large collections, consider parallel LINQ
var parallelQuery = products.AsParallel().Where(p => p.Price > 200);

Summary

LINQ is an incredibly powerful feature that makes working with collections in C# much more expressive and concise. By using LINQ, you can:

  • Filter collections with Where
  • Transform elements with Select
  • Sort with OrderBy and OrderByDescending
  • Group with GroupBy
  • Perform aggregations with Count, Sum, Average, Min, Max
  • Check conditions with Any and All
  • Combine collections with set operations like Union and Intersect

LINQ works with any collection implementing IEnumerable<T>, making it a universal tool for querying and manipulating data in your C# applications.

Exercises

  1. Create a list of at least 10 people with properties for Name, Age, and City. Write LINQ queries to:

    • Find all people above 30
    • Group people by city and count them
    • Find the average age per city
  2. Create a list of orders with properties for OrderId, CustomerName, OrderDate, and Total. Write LINQ queries to:

    • Find the top 3 highest value orders
    • Group orders by customer and calculate their total spending
    • Find orders from the last 30 days
  3. Challenge: Create two lists representing students and their course enrollments. Write a LINQ query that joins these collections to find which students are taking specific courses.

Additional Resources

Remember that mastering LINQ takes practice, but the investment is well worth it as it will dramatically improve the clarity and conciseness of your code.



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