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

  1. Reminder: type patterns with is
  2. Property patterns: { Available: true }
  3. Relational and logical patterns: and, or, not, >, <
  4. Switch expressions: the evolution of the classic switch
  5. Patterns in switch expressions: combining type and property
  6. Nullable reference types: #nullable enable, ?, and the ! operator
  7. Records as part of modern C#: a reminder and the positional pattern

  1. Reminder: type patterns with is

In 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.

  1. Property patterns: { Available: true }

A property pattern checks the value of one or more properties of an object, with no need for an intermediate variable:

if (item is { Available: true })
{
    Console.WriteLine("The item is available for lending.");
}

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.

  1. Relational and logical patterns: 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.

  1. Switch expressions: the evolution of the classic switch

In 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.

  1. 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}");
    }
}

  1. Nullable reference types: #nullable enable, ?, and the ! operator

You 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:

#nullable enable

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.

  1. 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:

record LoanSummary(string BookTitle, string MemberName, DateTime LoanDate);

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 case labels in a classic switch, 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/not with boolean expressions' &&/||/!: these are different syntaxes for different contexts; inside a pattern (is ..., or inside a switch expression's case), use the words and/or/not, not the symbolic operators.
  • Enabling #nullable enable halfway 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 be null reintroduces, through the back door, the same NullReferenceException risk that nullable reference types are meant to prevent.
  • Tip: prefer is not null over != null (and is null over == null) in modern code; besides being more readable, is can't be surprisingly overridden the way the == operator can be for specific types.

Exercises

  1. Write a method string Classify(LibraryItem item) that uses a switch expression with type and property patterns to return: "Available book" if it's a Book with Available: true, "Lent-out book" if it's a Book that isn't available, "Magazine" for any other Magazine case, and "Unknown" for any other type. Test it with an available Book, a lent-out one, and a Magazine.

  2. Enable #nullable enable at the top of a test file. Write a method Book? FindByTitle(List<Book> books, string title) that returns the first book whose Title matches, or null if none is found. Call the method twice (with a title that exists and one that doesn't) and, in each case, check with is not null before accessing its properties.

  3. 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.

© Copyright 2026. All rights reserved