With Library now bringing together the entire catalog and every member in collections, the
natural question is: how do you answer questions like "which books are available?", "how many
items are there by each author?", or "which loans have gone more than two weeks without being
returned?" With nothing but a foreach, every question would require writing a manual loop
with auxiliary variables. LINQ (Language Integrated Query) solves this with a set of
operators — filter, transform, sort, group — that combine expressively and work on any
IEnumerable<T> (the interface that closed the previous lesson). This lesson teaches you to
query BiblioTech's catalog and loans with LINQ, leaving manual loops behind for this kind of
task.
Contents
- What LINQ is and what problem it solves
- Method syntax:
Where,Select,OrderBy - Grouping results:
GroupBy - Operators that return a single value:
FirstOrDefault,Any,Count - Query syntax:
from...where...select - Deferred execution versus immediate execution
- Querying BiblioTech's catalog and loans
- What LINQ is and what problem it solves
LINQ adds, to any collection implementing IEnumerable<T> (List<T>, arrays,
Dictionary<TKey, TValue>.Values, and many more), a set of extension methods for querying it
declaratively: instead of describing how to traverse it step by step with a foreach, you
describe what result you want, and LINQ takes care of the traversal for you.
// Without LINQ: describing "how" to traverse, with a foreach and an auxiliary list
List<LibraryItem> available = new List<LibraryItem>();
foreach (LibraryItem item in library.Catalog)
{
if (item.Available)
{
available.Add(item);
}
}
// With LINQ: describing "what" result is wanted
List<LibraryItem> availableLinq = library.Catalog.Where(i => i.Available).ToList();Both fragments produce the same result, but the second directly expresses the intent ("the
items where Available is true"), without the intermediate mechanics of the loop and the
auxiliary list.
- Method syntax:
Where, Select, OrderBy
Where, Select, OrderByThe most commonly used LINQ operators, in their usual form (method syntax, chaining calls
with .):
| Operator | What it does | Example |
|---|---|---|
Where |
Filters elements by a condition (Func<T, bool>) |
catalog.Where(i => i.Available) |
Select |
Transforms each element into something else (projection) | catalog.Select(i => i.Title) |
OrderBy |
Sorts ascending by a key | catalog.OrderBy(i => i.Title) |
OrderByDescending |
Sorts descending | catalog.OrderByDescending(i => i.Title) |
List<LibraryItem> available = library.Catalog
.Where(i => i.Available)
.OrderBy(i => i.Title)
.ToList();
foreach (LibraryItem item in available)
{
Console.WriteLine(item.Title);
}This chain reads left to right like a pipeline: "take the catalog, keep only the available
ones, sort them by title, and turn it into a list." Select transforms each element, useful
when you don't need the whole object but just one of its pieces of data:
List<string> titles = library.Catalog.Select(i => i.Title).ToList();
// ["Hopscotch", "Ficciones", "National Geographic"]
- Grouping results:
GroupBy
GroupByGroupBy splits a collection's elements into groups by a key, something that with a manual
foreach would require a hand-built Dictionary<TKey, List<T>>:
var byAuthor = library.Catalog.GroupBy(i => i.Author);
foreach (var group in byAuthor)
{
Console.WriteLine($"{group.Key}: {group.Count()} item(s)");
foreach (LibraryItem item in group)
{
Console.WriteLine($" - {item.Title}");
}
}Each group is, at the same time, the key (group.Key, the author) and a collection of all
the elements with that key, over which you can iterate again or apply more LINQ. var is used
here because the exact type GroupBy returns (IEnumerable<IGrouping<string, LibraryItem>>)
is long to write and rarely needs to be named explicitly.
- Operators that return a single value:
FirstOrDefault, Any, Count
FirstOrDefault, Any, CountNot every query returns a collection; some answer with a single value:
| Operator | What it returns |
|---|---|
FirstOrDefault(condition) |
The first element satisfying the condition, or default (usually null) if none does |
First(condition) |
Same, but throws an exception if no element satisfies it |
Any(condition) |
true if at least one element satisfies the condition |
Count(condition) |
How many elements satisfy the condition |
Count() (no argument) |
The total number of elements |
LibraryItem? firstAvailable = library.Catalog.FirstOrDefault(i => i.Available);
bool hasMagazines = library.Catalog.Any(i => i is Magazine);
int totalBooks = library.Catalog.Count(i => i is Book);
Console.WriteLine(firstAvailable?.Title ?? "None available");
Console.WriteLine(hasMagazines); // True
Console.WriteLine(totalBooks); // 2FirstOrDefault is preferable to First whenever "finding nothing" is a normal, expected
situation (as here); reserve First for when the absence of a result would actually represent
a bug in the program's logic.
- Query syntax:
from...where...select
from...where...selectBesides method syntax, C# offers a query syntax, closer to SQL, which the compiler
translates internally to the same methods (Where, Select...) seen above:
var availableQuery =
from item in library.Catalog
where item.Available
orderby item.Title
select item;
foreach (LibraryItem item in availableQuery)
{
Console.WriteLine(item.Title);
}This query is exactly equivalent to
library.Catalog.Where(i => i.Available).OrderBy(i => i.Title): two different ways of writing
the same operation. In practice, method syntax is the more common one in modern C# code (it
fits better with long chains and with operators that have no direct equivalent in query
syntax, such as Count or Any); query syntax is used mostly when several sources are
combined (join), or when the team prefers it for readability. It's enough to recognize both
and know they're interchangeable.
- Deferred execution versus immediate execution
An important detail, easy to overlook: most LINQ operators (Where, Select, OrderBy...)
don't run at the moment they're written, but when the result is traversed (with foreach,
or by calling ToList()/ToArray()/Count()). This is called deferred execution:
var query = library.Catalog.Where(i => i.Available); // nothing has been evaluated yet
library.AddItem(new Book("The Aleph", "Jorge Luis Borges", "978-84-376-0497-8"));
foreach (LibraryItem item in query) // the filter runs HERE, over the already-updated catalog
{
Console.WriteLine(item.Title); // "The Aleph" is included too, even though it was added after "query" was defined
}Operators like ToList(), ToArray(), Count(), or FirstOrDefault(), on the other hand,
force immediate execution: the result is computed right then and stays fixed, unaffected
by later changes to the source collection. For everyday use, this practical rule is enough to
remember: if you're going to traverse the same result more than once, or need to "freeze" it at
a specific moment, add .ToList() at the end of the query.
- Querying BiblioTech's catalog and loans
With everything above, here's how the questions posed at the start of the lesson about
Library get answered:
// Which books are available, sorted by title?
List<LibraryItem> availableBooks = library.Catalog
.Where(i => i is Book && i.Available)
.OrderBy(i => i.Title)
.ToList();
// How many items are there by each author?
var itemsByAuthor = library.Catalog
.GroupBy(i => i.Author)
.Select(g => new { Author = g.Key, Count = g.Count() });
foreach (var entry in itemsByAuthor)
{
Console.WriteLine($"{entry.Author}: {entry.Count}");
}
// Which loans have gone more than 14 days without being returned (overdue)?
const int LoanLimitDays = 14;
List<Loan> overdueLoans = library.Loans
.Where(l => l.ReturnDate == null && (DateTime.Now - l.LoanDate).Days > LoanLimitDays)
.ToList();
foreach (Loan loan in overdueLoans)
{
Console.WriteLine($"Overdue: '{loan.Book.Title}' lent to {loan.Member.Name} on {loan.LoanDate:yyyy-MM-dd}");
}itemsByAuthor uses a feature you may not have seen yet: new { Author = g.Key, Count = g.Count() } creates an anonymous type, a throwaway object with only the properties you
need to display, with no class or separate record to declare — useful precisely for
intermediate results of a LINQ query that don't need a named type anywhere else in the
program.
The overdue loans query combines two conditions with &&: that it hasn't been returned yet
(ReturnDate == null) and that more days have passed than the loan limit since LoanDate.
This is exactly the kind of business question ("which loans need an overdue notice?") that
LINQ lets you express in a few lines, with no manual loop at all.
Common Mistakes and Tips
- Forgetting
.ToList()(or similar) when immediate execution is needed: if you modify the source collection between defining the query and traversing it, the result can change unexpectedly due to deferred execution; use.ToList()if you need to "fix" the result at a specific moment. - Chaining too many operators into one unreadable line: although technically valid, a very
long chain of
.Where().Select().OrderBy().GroupBy()...on a single line is hard to read; split the query across several lines, one operation per line, as in this lesson's examples. - Using
Firstwhen "not found" is a normal situation:Firstthrows anInvalidOperationExceptionif no element satisfies the condition; useFirstOrDefaultand checkis not null(recalling the Pattern Matching lesson) unless the absence of a result is, in fact, a program error. - Confusing
Count(LINQ operator) withCount(property ofList<T>):list.Count(no parentheses) is a fast property ofList<T>;list.Count(condition)(with parentheses and a predicate) is the LINQ operator, which traverses the collection to count how many elements satisfy the condition. Both coexist with no conflict because they have different signatures. - Tip: for simple filtering, transforming, and sorting tasks, method syntax is more common
and easier to debug step by step (you can comment out one line of the chain to see the
intermediate result); reserve query syntax for when you combine several sources with
join, or when the team explicitly prefers it.
Exercises
-
On
library.Catalog, use method syntax to get aList<string>with the titles of allAvailableitems, sorted alphabetically. -
On
library.Catalog, useGroupByto group items by author and show, for each author, how many items they have in the catalog. -
On
library.Loans, write a query that returns unreturned loans (ReturnDate == null) that have gone more than 14 days sinceLoanDate. Test it with at least one loan that satisfies the condition and one that doesn't.
Solutions
List<string> availableTitles = library.Catalog
.Where(i => i.Available)
.OrderBy(i => i.Title)
.Select(i => i.Title)
.ToList();
foreach (string title in availableTitles)
{
Console.WriteLine(title);
}
var byAuthor = library.Catalog.GroupBy(i => i.Author);
foreach (var group in byAuthor)
{
Console.WriteLine($"{group.Key}: {group.Count()} item(s)");
}
const int LoanLimitDays = 14;
List<Loan> overdue = library.Loans
.Where(l => l.ReturnDate == null && (DateTime.Now - l.LoanDate).Days > LoanLimitDays)
.ToList();
foreach (Loan loan in overdue)
{
Console.WriteLine($"Overdue: '{loan.Book.Title}' ({loan.Member.Name})");
}
To check the result, you could create a Loan with a manually simulated, older LoanDate
(recalling that, in the current model, LoanDate is automatically set to DateTime.Now in
the constructor), or simply reason about freshly created loans, which will never show up as
overdue at the same instant they run.
Conclusion
In this lesson you've learned to query collections with LINQ: filtering with Where,
transforming with Select, sorting with OrderBy, grouping with GroupBy, and getting
single values with FirstOrDefault, Any, and Count; you've also seen the alternative query
syntax and the difference between deferred and immediate execution. Library.Catalog and
Library.Loans can now be queried with the same expressiveness as a database, without having
written a single line of SQL.
That comparison is, in fact, no accident: in Module 5 (Working with Data), Entity Framework
will use LINQ as its main query language against a real database, so everything learned here
will carry over directly. Before getting there, one last piece remains in this module:
everything seen so far has been synchronous code, which blocks execution while it
completes. The module's last lesson, Asynchronous Programming, introduces async/await and
a first simulated version of LendBookAsync, preparing Library for the real I/O operations
(files, databases) coming in the next module.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
