C# LINQ (Language Integrated Query)
Overview
LINQ (Language Integrated Query) provides a unified query syntax for working with data from different sources. It allows you to write queries directly in C# code.
Basic LINQ Concepts
LINQ Query Syntax
csharp
using System.Linq;
// Data source
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Query syntax
var evenNumbers = from num in numbers
where num % 2 == 0
select num;
// Method syntax (equivalent)
var evenNumbersMethod = numbers.Where(num => num % 2 == 0);
// Execute query
foreach (var num in evenNumbers)
{
Console.WriteLine(num); // 2, 4, 6, 8, 10
}LINQ Method Syntax
csharp
List<string> words = new List<string>
{
"apple", "banana", "cherry", "date", "elderberry"
};
// Filtering
var longWords = words.Where(w => w.Length > 5);
// Projection
var wordLengths = words.Select(w => new { Word = w, Length = w.Length });
// Ordering
var sortedWords = words.OrderBy(w => w);
// Grouping
var wordsByLength = words.GroupBy(w => w.Length);LINQ Operators
Filtering Operators
csharp
public class LinqFiltering
{
public static void DemonstrateFiltering()
{
var products = new[]
{
new { Id = 1, Name = "Laptop", Price = 999.99m, Category = "Electronics" },
new { Id = 2, Name = "Book", Price = 19.99m, Category = "Books" },
new { Id = 3, Name = "Phone", Price = 699.99m, Category = "Electronics" },
new { Id = 4, Name = "Pen", Price = 2.99m, Category = "Office" }
};
// Where clause
var electronics = products.Where(p => p.Category == "Electronics");
var expensive = products.Where(p => p.Price > 500);
var cheapBooks = products.Where(p => p.Category == "Books" && p.Price < 20);
Console.WriteLine("Electronics:");
foreach (var item in electronics)
{
Console.WriteLine($"{item.Name}: ${item.Price}");
}
}
}Projection Operators
csharp
public class LinqProjection
{
public static void DemonstrateProjection()
{
var students = new[]
{
new { FirstName = "John", LastName = "Doe", Age = 20, Grade = 85 },
new { FirstName = "Jane", LastName = "Smith", Age = 22, Grade = 92 },
new { FirstName = "Bob", LastName = "Johnson", Age = 21, Grade = 78 }
};
// Select - transform to new type
var studentNames = students.Select(s => $"{s.FirstName} {s.LastName}");
var studentInfo = students.Select(s => new
{
FullName = $"{s.FirstName} {s.LastName}",
IsAdult = s.Age >= 18
});
// SelectMany - flatten collections
var classes = new[]
{
new { Name = "Math", Students = new[] { "Alice", "Bob" } },
new { Name = "Science", Students = new[] { "Charlie", "David", "Eve" } }
};
var allStudents = classes.SelectMany(c => c.Students);
Console.WriteLine("All students:");
foreach (var student in allStudents)
{
Console.WriteLine(student);
}
}
}Ordering Operators
csharp
public class LinqOrdering
{
public static void DemonstrateOrdering()
{
var people = new[]
{
new { Name = "Alice", Age = 30, Salary = 50000 },
new { Name = "Bob", Age = 25, Salary = 45000 },
new { Name = "Charlie", Age = 35, Salary = 60000 },
new { Name = "David", Age = 28, Salary = 52000 }
};
// OrderBy
var byAge = people.OrderBy(p => p.Age);
var byName = people.OrderBy(p => p.Name);
// OrderByDescending
var bySalaryDesc = people.OrderByDescending(p => p.Salary);
// ThenBy for secondary sorting
var byAgeThenName = people
.OrderBy(p => p.Age)
.ThenBy(p => p.Name);
Console.WriteLine("People ordered by age:");
foreach (var person in byAge)
{
Console.WriteLine($"{person.Name}, Age: {person.Age}");
}
}
}Grouping Operators
csharp
public class LinqGrouping
{
public static void DemonstrateGrouping()
{
var products = new[]
{
new { Name = "Laptop", Category = "Electronics", Price = 999.99m },
new { Name = "Mouse", Category = "Electronics", Price = 29.99m },
new { Name = "Book", Category = "Books", Price = 19.99m },
new { Name = "Pen", Category = "Office", Price = 2.99m },
new { Name = "Phone", Category = "Electronics", Price = 699.99m }
};
// GroupBy
var groupedByCategory = products.GroupBy(p => p.Category);
foreach (var group in groupedByCategory)
{
Console.WriteLine($"Category: {group.Key}");
foreach (var product in group)
{
Console.WriteLine($" {product.Name}: ${product.Price}");
}
}
// GroupBy with projection
var categoryStats = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
TotalPrice = g.Sum(p => p.Price),
AveragePrice = g.Average(p => p.Price)
});
Console.WriteLine("\nCategory statistics:");
foreach (var stat in categoryStats)
{
Console.WriteLine($"{stat.Category}: {stat.Count} items, " +
$"Avg: ${stat.AveragePrice:F2}");
}
}
}Set Operations
csharp
public class LinqSetOperations
{
public static void DemonstrateSetOperations()
{
var set1 = new[] { 1, 2, 3, 4, 5 };
var set2 = new[] { 4, 5, 6, 7, 8 };
// Union
var union = set1.Union(set2);
Console.WriteLine($"Union: {string.Join(", ", union)}");
// Intersection
var intersection = set1.Intersect(set2);
Console.WriteLine($"Intersection: {string.Join(", ", intersection)}");
// Except
var except = set1.Except(set2);
Console.WriteLine($"Except: {string.Join(", ", except)}");
// Distinct
var numbers = new[] { 1, 2, 2, 3, 3, 3, 4 };
var distinct = numbers.Distinct();
Console.WriteLine($"Distinct: {string.Join(", ", distinct)}");
}
}LINQ with Collections
LINQ with Lists
csharp
public class LinqWithLists
{
public static void DemonstrateListOperations()
{
var employees = new List<Employee>
{
new Employee { Id = 1, Name = "Alice", Department = "IT", Salary = 60000 },
new Employee { Id = 2, Name = "Bob", Department = "HR", Salary = 50000 },
new Employee { Id = 3, Name = "Charlie", Department = "IT", Salary = 65000 },
new Employee { Id = 4, Name = "David", Department = "Finance", Salary = 55000 }
};
// Find specific employee
var employee = employees.FirstOrDefault(e => e.Id == 2);
// Filter by department
var itEmployees = employees.Where(e => e.Department == "IT").ToList();
// Calculate statistics
var totalSalary = employees.Sum(e => e.Salary);
var averageSalary = employees.Average(e => e.Salary);
var maxSalary = employees.Max(e => e.Salary);
// Check conditions
var hasHighEarner = employees.Any(e => e.Salary > 70000);
var allHaveSalary = employees.All(e => e.Salary > 0);
Console.WriteLine($"Total salary: ${totalSalary:N2}");
Console.WriteLine($"Average salary: ${averageSalary:N2}");
Console.WriteLine($"Max salary: ${maxSalary:N2}");
}
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Department { get; set; }
public decimal Salary { get; set; }
}LINQ with Dictionaries
csharp
public class LinqWithDictionaries
{
public static void DemonstrateDictionaryOperations()
{
var productPrices = new Dictionary<string, decimal>
{
["Laptop"] = 999.99m,
["Mouse"] = 29.99m,
["Keyboard"] = 79.99m,
["Monitor"] = 299.99m
};
// Filter dictionary
var expensive = productPrices.Where(kvp => kvp.Value > 100);
// Transform to different type
var productList = productPrices
.Select(kvp => new { Name = kvp.Key, Price = kvp.Value })
.OrderBy(p => p.Price);
// Group by price range
var priceGroups = productPrices
.GroupBy(kvp => kvp.Value < 50 ? "Cheap" :
kvp.Value < 200 ? "Medium" : "Expensive");
Console.WriteLine("Products by price range:");
foreach (var group in priceGroups)
{
Console.WriteLine($"{group.Key}:");
foreach (var item in group)
{
Console.WriteLine($" {item.Key}: ${item.Value}");
}
}
}
}Advanced LINQ
LINQ with Custom Comparers
csharp
public class CustomComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
}
public int GetHashCode(string obj)
{
return obj?.ToUpperInvariant().GetHashCode() ?? 0;
}
}
public class LinqCustomComparers
{
public static void DemonstrateCustomComparers()
{
var words = new[] { "Apple", "apple", "Banana", "banana", "Cherry" };
// Case-insensitive distinct
var distinctWords = words.Distinct(new CustomComparer());
Console.WriteLine("Distinct words (case-insensitive):");
foreach (var word in distinctWords)
{
Console.WriteLine(word);
}
}
}LINQ with Anonymous Types
csharp
public class LinqAnonymousTypes
{
public static void DemonstrateAnonymousTypes()
{
var people = new[]
{
new { FirstName = "John", LastName = "Doe", Age = 30, City = "New York" },
new { FirstName = "Jane", LastName = "Smith", Age = 25, City = "Los Angeles" },
new { FirstName = "Bob", LastName = "Johnson", Age = 35, City = "Chicago" }
};
// Create anonymous type
var summary = people
.GroupBy(p => p.City)
.Select(g => new
{
City = g.Key,
Count = g.Count(),
AverageAge = g.Average(p => p.Age),
Names = g.Select(p => $"{p.FirstName} {p.LastName}").ToList()
});
foreach (var city in summary)
{
Console.WriteLine($"City: {city.City}");
Console.WriteLine($" Count: {city.Count}");
Console.WriteLine($" Average Age: {city.AverageAge:F1}");
Console.WriteLine($" Names: {string.Join(", ", city.Names)}");
}
}
}LINQ to XML
XML Querying with LINQ
csharp
using System.Xml.Linq;
public class LinqToXml
{
public static void DemonstrateXmlQuerying()
{
// Create XML document
var xml = XElement.Parse(@"
<books>
<book id='1' category='Fiction'>
<title>The Great Gatsby</title>
<author>F. Scott Fitzgerald</author>
<price>12.99</price>
</book>
<book id='2' category='Non-Fiction'>
<title>Thinking, Fast and Slow</title>
<author>Daniel Kahneman</author>
<price>16.99</price>
</book>
<book id='3' category='Fiction'>
<title>1984</title>
<author>George Orwell</author>
<price>14.99</price>
</book>
</books>");
// Query XML with LINQ
var fictionBooks = from book in xml.Elements("book")
where (string)book.Attribute("category") == "Fiction"
select new
{
Title = (string)book.Element("title"),
Author = (string)book.Element("author"),
Price = (decimal)book.Element("price"),
Id = (int)book.Attribute("id")
};
Console.WriteLine("Fiction books:");
foreach (var book in fictionBooks)
{
Console.WriteLine($"{book.Title} by {book.Author} - ${book.Price}");
}
}
}Performance Considerations
Deferred vs Immediate Execution
csharp
public class LinqPerformance
{
public static void DemonstrateExecution()
{
var numbers = Enumerable.Range(1, 1000000);
// Deferred execution - query is not executed yet
var query = numbers.Where(n => n % 2 == 0)
.Select(n => n * 2);
// Immediate execution - query is executed now
var result = query.ToList();
// Force immediate execution with ToList(), ToArray(), Count(), etc.
var count = numbers.Count(n => n > 500000);
Console.WriteLine($"Count: {count}");
Console.WriteLine($"First 5 results: {string.Join(", ", result.Take(5))}");
}
}Optimizing LINQ Queries
csharp
public class LinqOptimization
{
public static void DemonstrateOptimization()
{
var largeList = Enumerable.Range(1, 1000000).ToList();
// Bad: Multiple enumeration
// var count = largeList.Where(n => n > 500000).Count();
// var sum = largeList.Where(n => n > 500000).Sum();
// Good: Single enumeration
var filtered = largeList.Where(n => n > 500000);
var count = filtered.Count();
var sum = filtered.Sum();
// Better: Use specific operators
var count2 = largeList.Count(n => n > 500000);
var sum2 = largeList.Where(n => n > 500000).Sum();
Console.WriteLine($"Count: {count}, Sum: {sum}");
}
}Practical Examples
Data Analysis Example
csharp
public class DataAnalysis
{
public static void AnalyzeSalesData()
{
var sales = new[]
{
new { Date = DateTime.Parse("2023-01-01"), Product = "Laptop", Amount = 1200, Region = "North" },
new { Date = DateTime.Parse("2023-01-02"), Product = "Mouse", Amount = 30, Region = "South" },
new { Date = DateTime.Parse("2023-01-03"), Product = "Laptop", Amount = 1100, Region = "East" },
new { Date = DateTime.Parse("2023-01-04"), Product = "Keyboard", Amount = 80, Region = "North" },
new { Date = DateTime.Parse("2023-01-05"), Product = "Mouse", Amount = 35, Region = "West" }
};
// Sales by region
var salesByRegion = sales
.GroupBy(s => s.Region)
.Select(g => new
{
Region = g.Key,
TotalSales = g.Sum(s => s.Amount),
AverageSale = g.Average(s => s.Amount),
Count = g.Count()
})
.OrderByDescending(r => r.TotalSales);
Console.WriteLine("Sales by Region:");
foreach (var region in salesByRegion)
{
Console.WriteLine($"{region.Region}: Total=${region.TotalSales:N2}, " +
$"Avg=${region.AverageSale:N2}, Count={region.Count}");
}
// Sales by product
var salesByProduct = sales
.GroupBy(s => s.Product)
.Select(g => new
{
Product = g.Key,
TotalRevenue = g.Sum(s => s.Amount),
UnitsSold = g.Count(),
AveragePrice = g.Average(s => s.Amount)
});
Console.WriteLine("\nSales by Product:");
foreach (var product in salesByProduct)
{
Console.WriteLine($"{product.Product}: Revenue=${product.TotalRevenue:N2}, " +
$"Units={product.UnitsSold}, Avg Price=${product.AveragePrice:N2}");
}
}
}Report Generation
csharp
public class ReportGenerator
{
public static void GenerateEmployeeReport()
{
var employees = new[]
{
new { Name = "Alice", Department = "IT", Salary = 60000, HireDate = DateTime.Parse("2020-01-15") },
new { Name = "Bob", Department = "HR", Salary = 50000, HireDate = DateTime.Parse("2019-03-20") },
new { Name = "Charlie", Department = "IT", Salary = 65000, HireDate = DateTime.Parse("2021-06-10") },
new { Name = "David", Department = "Finance", Salary = 55000, HireDate = DateTime.Parse("2018-11-05") }
};
// Department summary
var departmentSummary = employees
.GroupBy(e => e.Department)
.Select(g => new
{
Department = g.Key,
EmployeeCount = g.Count(),
TotalSalary = g.Sum(e => e.Salary),
AverageSalary = g.Average(e => e.Salary),
NewestHire = g.Max(e => e.HireDate),
Employees = g.OrderBy(e => e.Name).ToList()
});
Console.WriteLine("=== Employee Report ===");
foreach (var dept in departmentSummary)
{
Console.WriteLine($"\nDepartment: {dept.Department}");
Console.WriteLine($"Employees: {dept.EmployeeCount}");
Console.WriteLine($"Total Salary: ${dept.TotalSalary:N2}");
Console.WriteLine($"Average Salary: ${dept.AverageSalary:N2}");
Console.WriteLine($"Newest Hire: {dept.NewestHire:yyyy-MM-dd}");
Console.WriteLine("Employees:");
foreach (var emp in dept.Employees)
{
int yearsOfService = (int)((DateTime.Now - emp.HireDate).TotalDays / 365);
Console.WriteLine($" {emp.Name} - {yearsOfService} years");
}
}
}
}Best Practices
LINQ Guidelines
csharp
// Good: Use appropriate LINQ methods
var result = data.Where(x => x.IsValid)
.OrderBy(x => x.Name)
.Take(10);
// Bad: Complex nested queries
var badResult = data.Where(x =>
data.Where(y => y.Id == x.ParentId)
.Select(y => y.Name)
.FirstOrDefault() == "Expected");
// Good: Break complex queries into steps
var parentIds = data.Where(x => x.IsValid).Select(x => x.ParentId);
var parentNames = data.Where(x => parentIds.Contains(x.Id))
.ToDictionary(x => x.Id, x => x.Name);Performance Tips
csharp
// Good: Use specific operators when possible
var count = collection.Count(x => x.IsActive);
// Bad: Use Where().Count() when Count() with predicate exists
var badCount = collection.Where(x => x.IsActive).Count();
// Good: Use AsEnumerable() for database queries
var dbResults = dbContext.Products
.Where(p => p.Price > 100)
.AsEnumerable()
.Select(p => new { p.Name, FormattedPrice = $"${p.Price:N2}" });Summary
In this chapter, you learned:
- LINQ query syntax vs method syntax
- Filtering, projection, ordering, and grouping operators
- Set operations and aggregation
- LINQ with different collection types
- Advanced features like custom comparers
- LINQ to XML
- Performance considerations and best practices
LINQ provides a powerful, consistent way to query and manipulate data from various sources. Mastering LINQ will make you more productive when working with collections and data in C#.