In the Polymorphism lesson (Module 3) you already used is Book book to check the actual
type of a LibraryItem at run time, and in Switch Statements (Module 2) you got a first
preview of switch expressions. Both ideas are part of a broader feature of modern C#:
pattern matching, which lets you check not just the type of a value, but also its
properties, its ranges, and logical combinations between conditions, all in a compact syntax.
This lesson brings together pattern matching, switch expressions, nullable reference types,
and a reminder about records, as the set of features that define the style of C# today, and
applies them to classify and filter BiblioTech's items.
Contents
- Reminder: type patterns with
is - Property patterns:
{ Available: true } - Relational and logical patterns:
and,or,not,>,< - Switch expressions: the evolution of the classic
switch - Patterns in switch expressions: combining type and property
- Nullable reference types:
#nullable enable,?, and the!operator - Records as part of modern C#: a reminder and the positional pattern
- Reminder: type patterns with
is
isIn the Polymorphism lesson you already wrote code like this, using a type pattern:
LibraryItem item = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
if (item is Book book)
{
Console.WriteLine($"It's a book with ISBN {book.Isbn}");
}item is Book book checks, at run time, whether item is (or inherits from) Book, and if
it is, it automatically declares a new variable book of type Book, already converted,
available inside the block. This is already pattern matching: a type pattern is the simplest
of all the patterns C# offers, and from here the lesson introduces more expressive patterns
that combine with it.
- Property patterns:
{ Available: true }
{ Available: true }A property pattern checks the value of one or more properties of an object, with no need for an intermediate variable:
item is { Available: true } reads as "if item has an Available property equal to
true." This pattern can be combined with a type pattern, checking both the concrete type
and one of its properties at once:
if (item is Book { Available: true } book)
{
Console.WriteLine($"'{book.Title}' is an available book.");
}Here, three things are checked at once in a single expression: that item is (at run time) a
Book, that its Available is true, and, in addition, the already-converted variable
book is obtained — all in one line that would previously have required a nested if.
- Relational and logical patterns:
and, or, not, >, <
and, or, not, >, <Relational patterns compare a numeric value using the operators <, >, <=, >=
directly inside a pattern:
Magazine magazine1 = new Magazine("National Geographic", "Various authors", 302);
if (magazine1 is Magazine { IssueNumber: > 100 })
{
Console.WriteLine("A long-running magazine (more than 100 issues).");
}Logical patterns combine several patterns with the keywords and, or, and not
(instead of &&, ||, !, which are reserved for regular boolean expressions):
bool isEstablishedMagazine = magazine1 is Magazine { IssueNumber: > 50 and < 1000 };
bool isAvailableAndNotMagazine = item is not Magazine and { Available: true };| Pattern | Example | Meaning |
|---|---|---|
| Type | item is Book |
Is it (at run time) a Book? |
| Type with variable | item is Book book |
Same as above, and also assigns book |
| Property | item is { Available: true } |
Does the Available property equal true? |
| Relational | issueNumber is > 100 |
Is the value greater than 100? |
Logical and/or/not |
item is not Magazine and { Available: true } |
Combines several patterns with logic |
| Combined (type + property) | item is Book { Available: true } book |
Type, property, and variable all at once |
These patterns aren't limited to if: they can be used in any is expression, and — most
commonly in modern code — inside a switch expression, the topic of the next section.
- Switch expressions: the evolution of the classic
switch
switchIn the Switch Statements lesson (Module 2) you got a first preview of switch expressions:
a way of writing a switch that returns a value directly, with no repeated case/break,
using => for each possible result:
string category = "Novel";
string section = category switch
{
"Novel" => "Fiction - Floor 1",
"Essay" => "Non-fiction - Floor 2",
"Poetry" => "Poetry - Floor 1",
_ => "Unrecognized category"
};Classic switch (statement) |
Switch expression | |
|---|---|---|
| Does it return a value directly? | No; you must assign inside each case |
Yes; the whole expression evaluates to a value |
| Keyword per case | case value: ... break; |
pattern => value, |
| Default case | default: |
_ (underscore) |
| Verbosity | Higher (braces, break in every case) |
Lower (generally one line per case) |
| Compatible with patterns (type, property, relational...) | Limited | Full |
The most important difference for this lesson is the last row: while the classic switch
only compares exact equality against constant values, a switch expression accepts any
pattern from sections 1 through 3, which makes it the natural tool for classifying objects
like BiblioTech's according to their actual type and their properties.
- Patterns in switch expressions: combining type and property
Here's how a LibraryItem is classified by combining type and property patterns inside a
switch expression:
string Classify(LibraryItem item) => item switch
{
Book { Available: true } => $"Available book: {item.Title}",
Book => $"Lent-out book: {item.Title}",
Magazine { IssueNumber: > 100 } => $"Long-running magazine: {item.Title}",
Magazine => $"Magazine: {item.Title}",
_ => "Unrecognized item type"
};Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Magazine magazine1 = new Magazine("National Geographic", "Various authors", 302);
book1.Lend();
Console.WriteLine(Classify(book1)); // "Lent-out book: Hopscotch"
Console.WriteLine(Classify(magazine1)); // "Long-running magazine: National Geographic"The order of the cases matters, just as in a classic switch: C# evaluates each pattern from
top to bottom and runs the first one that matches. That's why Book { Available: true } must
come before plain Book: if Book were the first case, it would always match before
Available was even checked, and the more specific case would never be reached.
Filtering an entire catalog list reuses exactly the same pattern, this time inside a boolean expression with LINQ (which you'll study in depth in a later lesson of this module):
List<LibraryItem> catalog = new List<LibraryItem> { book1, magazine1 };
foreach (LibraryItem item in catalog)
{
if (item is Book and { Available: true })
{
Console.WriteLine($"Available for lending: {item.Title}");
}
}
- Nullable reference types:
#nullable enable, ?, and the ! operator
#nullable enable, ?, and the ! operatorYou already used Book? in the Polymorphism lesson (Book? castBook = firstItem as Book;)
without pausing on why the ? was there. Modern C# lets you enable nullable reference
types, a feature that makes the compiler distinguish, for reference types (class, not just
string/int?), between "can never be null" and "can be null," warning you when the code
doesn't respect that distinction.
It's enabled (many new .NET projects come with it enabled by default) with this directive at the top of the file:
With this directive active, a type like Book means "never null" unless it's explicitly
declared as Book?:
Book? FindBookByIsbn(List<Book> books, string isbn)
{
foreach (Book book in books)
{
if (book.Isbn == isbn)
{
return book;
}
}
return null; // valid: the return type is Book?, which allows null
}Book? result = FindBookByIsbn(bookCatalog, "978-84-376-0495-4");
if (result is not null)
{
Console.WriteLine(result.Title); // inside the "if", the compiler knows it isn't null
}The ! operator (called the null-forgiving operator) tells the compiler "trust me, I know
this value isn't null here, even though its type is nullable," silencing the warning without
adding any real check at run time:
Book safeBook = FindBookByIsbn(bookCatalog, "978-84-376-0495-4")!;
// Warning silenced, but if the result were actually null, this would throw a
// NullReferenceException at run time when safeBook is used later on.| Tool | What it expresses |
|---|---|
Book (without ?) |
The compiler expects it to never be null; it warns if it detects otherwise |
Book? |
It can be null; the compiler requires you to check it before using it with confidence |
is not null / is null |
The recommended pattern for checking nullity (clearer than != null) |
value! |
"I trust this isn't null here," with no real check; use it sparingly |
It's important to understand that nullable reference types are, above all, a compiler
aid at compile time (warnings, not hard errors in most cases): they don't prevent an actual
null from arriving at run time if the ! operator is misused, but they help enormously in
catching potential NullReferenceExceptions before running the program.
- Records as part of modern C#: a reminder and the positional pattern
The last lesson of Module 3 introduced record, with LoanSummary as the example:
Records, together with pattern matching, switch expressions, and nullable reference types, are part of the set of features that define modern C#: they all share the goal of expressing more meaning with less code, and of catching more errors at compile time. A direct connection between records and pattern matching is the positional pattern, which deconstructs a record directly into variables, using the same order as its constructor:
LoanSummary summary = new LoanSummary("Hopscotch", "Ana Martinez", new DateTime(2026, 1, 10));
if (summary is LoanSummary(var bookTitle, var memberName, _))
{
Console.WriteLine($"{memberName} has '{bookTitle}' on loan");
}LoanSummary(var bookTitle, var memberName, _) extracts the record's three positional
properties into two new variables, discarding the third with _ (the same underscore you
already know as the "default" case of a switch expression, here used as "I don't care about
this value"). This automatic deconstruction ability is another advantage records provide "for
free," with no extra code, thanks to their positional syntax.
Common Mistakes and Tips
- Forgetting the order in a switch expression with type patterns: just as with stacked
caselabels in a classicswitch, a more general case placed before a more specific one "shadows" the second; always place the more restrictive patterns (Book { Available: true }) before the more general ones (Book). - Confusing patterns'
and/or/notwith boolean expressions'&&/||/!: these are different syntaxes for different contexts; inside a pattern (is ..., or inside a switch expression'scase), use the wordsand/or/not, not the symbolic operators. - Enabling
#nullable enablehalfway through a project and expecting zero warnings: turning it on over existing code is normal (and expected) to surface warnings in places that need review; don't systematically ignore those warnings — review them one by one. - Overusing the
!operator: using it to silence any warning without analyzing whether the value could really benullreintroduces, through the back door, the sameNullReferenceExceptionrisk that nullable reference types are meant to prevent. - Tip: prefer
is not nullover!= null(andis nullover== null) in modern code; besides being more readable,iscan't be surprisingly overridden the way the==operator can be for specific types.
Exercises
-
Write a method
string Classify(LibraryItem item)that uses a switch expression with type and property patterns to return:"Available book"if it's aBookwithAvailable: true,"Lent-out book"if it's aBookthat isn't available,"Magazine"for any otherMagazinecase, and"Unknown"for any other type. Test it with an availableBook, a lent-out one, and aMagazine. -
Enable
#nullable enableat the top of a test file. Write a methodBook? FindByTitle(List<Book> books, string title)that returns the first book whoseTitlematches, ornullif none is found. Call the method twice (with a title that exists and one that doesn't) and, in each case, check withis not nullbefore accessing its properties. -
Given a
record LoanSummary(string BookTitle, string MemberName, DateTime LoanDate), create a sample object and use a positional pattern (is LoanSummary(var title, var name, var date)) to extract its three values into new variables and display them separately.
Solutions
string Classify(LibraryItem item) => item switch
{
Book { Available: true } => "Available book",
Book => "Lent-out book",
Magazine => "Magazine",
_ => "Unknown"
};
Book availableBook = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Book lentBook = new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
lentBook.Lend();
Magazine magazine1 = new Magazine("National Geographic", "Various authors", 302);
Console.WriteLine(Classify(availableBook)); // "Available book"
Console.WriteLine(Classify(lentBook)); // "Lent-out book"
Console.WriteLine(Classify(magazine1)); // "Magazine"
#nullable enable
Book? FindByTitle(List<Book> books, string title)
{
foreach (Book book in books)
{
if (book.Title == title)
{
return book;
}
}
return null;
}
List<Book> books = new List<Book>
{
new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4")
};
Book? found = FindByTitle(books, "Hopscotch");
if (found is not null)
{
Console.WriteLine(found.Isbn); // Ok, inside the "if" it's already known not to be null
}
Book? notFound = FindByTitle(books, "Another title");
if (notFound is null)
{
Console.WriteLine("The book was not found.");
}
record LoanSummary(string BookTitle, string MemberName, DateTime LoanDate);
LoanSummary summary = new LoanSummary("Hopscotch", "Ana Martinez", new DateTime(2026, 1, 10));
if (summary is LoanSummary(var title, var name, var date))
{
Console.WriteLine($"Title: {title}");
Console.WriteLine($"Member: {name}");
Console.WriteLine($"Date: {date:yyyy-MM-dd}");
}
Conclusion
In this lesson you've extended the is and switch you already knew with type, property,
relational, and logical patterns; you've learned to use switch expressions with those same
patterns to classify objects compactly; you've enabled nullable reference types so the
compiler helps you prevent null references; and you've seen records, already familiar from
Module 3, deconstructed with a positional pattern. Together, all of this forms the idiomatic
style of C# today.
Up to now, every BiblioTech example has worked with individual objects or, at most, with a
makeshift List<LibraryItem> inside a single method. The next lesson takes a step back to
introduce generics: the feature that makes List<T> possible in the first place, and
that will let you write your own reusable classes and methods for any data type, before
building, in the Collections lesson, the complete Library class.
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
