LINQ (Language Integrated Query)

LINQ (Language Integrated Query)

LINQ is one of C#'s most powerful features. It provides a unified way to query and manipulate data from different sources—whether it's an in-memory List, an XML document, or a SQL database—using a syntax that is consistent and readable.


1. The Two Flavors: Method vs. Query Syntax

LINQ can be written in two ways. Both are functionally identical, but one might be more readable than the other depending on the complexity of the query.

Method Syntax (Fluent API)

Uses extension methods and lambda expressions. This is the most common way to write LINQ.

var highScores = scores.Where(s => s > 90).OrderBy(s => s);

Query Syntax (SQL-like)

Uses keywords that look like SQL. The compiler actually converts this into Method Syntax behind the scenes.

var highScores = from s in scores
                 where s > 90
                 orderby s
                 select s;

2. IEnumerable vs. IQueryable: The "Where" Matters

Understanding the difference between these two interfaces is critical for performance, especially when working with databases.

IEnumerable<T> (In-Memory)

  • Execution: All filtering happens locally in your application's memory.
  • Best for: Small to medium data already loaded into RAM (Lists, Arrays).

IQueryable<T> (External Data)

  • Execution: The query is translated into the provider's language (e.g., SQL) and executed on the server.
  • Best for: Querying large databases where you only want to pull specific records.

3. Essential Extension Methods (Deep Dive)

A. Filtering & Type Checking

1. Where(predicate)

Filters a sequence based on a boolean condition.

// Use Case: Finding active users
var activeUsers = users.Where(u => u.IsActive && u.LastLogin > DateTime.Now.AddDays(-30));

2. OfType<T>()

Filters a sequence to only return elements of a specific type. Useful for mixed collections.

// Use Case: Getting only 'Manager' objects from a list of 'Employee'
var managers = staff.OfType<Manager>();

B. Projection (Transforming)

3. Select(selector)

Transforms each element into a new form (often called "Mapping").

// Use Case: Extracting just email addresses from a User list
var emails = users.Select(u => u.Email);

4. SelectMany(selector)

Flattens a sequence of sequences. Think of it as "Map and then Flatten."

// Use Case: Getting every OrderItem from every Order in a list
var allItems = orders.SelectMany(o => o.Items);

C. Ordering

5. OrderBy / OrderByDescending

Sorts elements in ascending or descending order.

// Use Case: Sorting products by price
var cheapestFirst = products.OrderBy(p => p.Price);
var priciestFirst = products.OrderByDescending(p => p.Price);

6. ThenBy / ThenByDescending

Performs a secondary sort after an initial OrderBy.

// Use Case: Sorting by Last Name, then by First Name
var sortedNames = users.OrderBy(u => u.LastName).ThenBy(u => u.FirstName);

D. Grouping & Joining

7. GroupBy(keySelector)

Groups elements into buckets based on a common key.

// Use Case: Grouping products by category
var categories = products.GroupBy(p => p.Category);
foreach (var group in categories) {
    Console.WriteLine($"Category: {group.Key}, Total: {group.Count()}");
}

8. Join(...)

Correlates elements from two different sources based on a key (Inner Join).

// Use Case: Matching Employees to their Departments
var report = employees.Join(departments, 
    emp => emp.DeptId, 
    dept => dept.Id, 
    (emp, dept) => new { emp.Name, dept.DeptName });

E. Quantifiers (Boolean Checks)

9. Any(predicate)

Returns true if at least one element matches. Extremely fast because it stops as soon as a match is found.

// Use Case: Checking if an inventory has ANY out-of-stock items
bool hasShortage = stock.Any(s => s.Quantity == 0);

10. All(predicate)

Returns true if every element matches.

// Use Case: Ensuring ALL students passed the exam
bool allPassed = students.All(s => s.Grade >= 60);

11. Contains(value)

Checks if a specific instance or value exists in the collection.

// Use Case: Checking if a specific role is in the user's roles
bool isAdmin = userRoles.Contains("Admin");

F. Aggregates (Math)

12. Count() / Sum() / Average()

Calculates the count, total, or mean of values.

// Use Case: Financial reporting
int transactionCount = transactions.Count();
decimal totalRevenue = transactions.Sum(t => t.Amount);
double avgRating = reviews.Average(r => r.Score);

13. Min() / Max()

Finds the smallest or largest value.

// Use Case: Finding the highest score in a game
int highscore = playerScores.Max();

14. Aggregate(func)

Performs a custom cumulative operation (like a running total or complex string build).

// Use Case: Building a comma-separated string
string list = fruits.Aggregate((workingSentence, next) => workingSentence + ", " + next);

G. Element Operators

15. First() / FirstOrDefault()

Returns the first item. FirstOrDefault is safer because it returns null if empty instead of crashing.

// Use Case: Getting the most recent order
var lastOrder = orders.OrderByDescending(o => o.Date).FirstOrDefault();

16. Single() / SingleOrDefault()

Returns exactly one item. Throws an exception if 0 or 2+ items are found.

// Use Case: Getting a user by a unique ID
var user = users.SingleOrDefault(u => u.Id == 123);

17. Distinct()

Removes duplicate values from a sequence.

// Use Case: Getting a list of unique cities from a customer database
var uniqueCities = customers.Select(c => c.City).Distinct();

H. Conversion & Execution

18. ToList() / ToArray()

Forces the query to run immediately and stores results in a standard collection.

// Use Case: Snapping a "point-in-time" copy of data
var cachedList = query.ToList();

19. ToDictionary(keySelector)

Converts a collection into a lookup dictionary.

// Use Case: Creating an ID-to-User lookup for fast access
var userMap = users.ToDictionary(u => u.Id);
var specificUser = userMap[123];

4. Practical Example: Working with a Dictionary

You can use LINQ to filter and transform Dictionaries effectively.

var inventory = new Dictionary<string, int> 
{
    { "Apples", 50 },
    { "Bananas", 10 },
    { "Cherries", 100 }
};

// Find items with low stock and get their names as a List
var lowStock = inventory
    .Where(kvp => kvp.Value < 20)
    .Select(kvp => kvp.Key)
    .ToList();

5. Deferred Execution: Why .ToList() matters

LINQ queries use Deferred Execution. This means the query is not run until you actually start iterating over it (e.g., in a foreach loop or by calling .ToList()).

var query = students.Where(s => s.Age > 20); // Query defined, but NOT executed yet.

students.Add(new Student { Name = "New Guy", Age = 25 });

// Execution happens HERE. "New Guy" WILL be included!
foreach(var s in query) { ... }