Skip to main content

C# Pattern Matching: Complete Guide (Switch, List Patterns)

Master C# pattern matching with this complete guide. Learn switch expressions, property patterns, and list patterns with runnable code examples. Start coding today!

C# pattern matching is one of the most powerful features added to the language in recent years, and if you are not using it yet, your code is probably more verbose and harder to read than it needs to be. In this complete guide to C# pattern matching, you will learn how to use switch expressions, property patterns, list patterns, and more — with practical, runnable code examples you can drop straight into your projects. Whether you are a beginner searching for how pattern matching works, an intermediate developer looking for best practices, or a senior engineer wanting advanced techniques, this tutorial has you covered.

What Is Pattern Matching in C#?

Pattern matching is a technique that tests whether a value has a certain shape, and when it does, extracts information from it. Instead of writing long chains of if/else statements or manual type casts, you describe the pattern you are looking for and let the compiler do the heavy lifting. It was first introduced in C# 7.0 with type patterns and has grown steadily through C# 8, 9, 10, and 11 into a rich, expressive syntax.

The core idea is simple: you match an input value against a series of patterns, and the first pattern that matches determines the result. This makes your code more declarative — you say what you want, not how to check for it step by step. The result is fewer bugs, less boilerplate, and code that reads almost like plain English.

Why Pattern Matching Matters

Before pattern matching, checking a type and casting required two separate operations that could easily drift out of sync. Consider the classic pre-C# 7 approach:

object shape = GetShape();

if (shape is Circle)
{
    var circle = (Circle)shape;   // redundant cast
    Console.WriteLine($"Area: {Math.PI * circle.Radius * circle.Radius}");
}

This is repetitive and error-prone. With the type pattern, the check and the cast become a single, safe expression:

object shape = GetShape();

if (shape is Circle circle)
{
    Console.WriteLine($"Area: {Math.PI * circle.Radius * circle.Radius}");
}

The variable circle is only in scope and assigned when the pattern matches. This single improvement eliminated an entire category of invalid-cast bugs, and it is the foundation everything else builds on.

C# Switch Expressions: The Modern Way

The C# switch expression, introduced in C# 8.0, is arguably the most-loved pattern matching feature. Unlike the traditional switch statement, a switch expression returns a value, uses a concise => arrow syntax, and requires no break statements. Here is a direct comparison.

The old switch statement:

string GetDayType(DayOfWeek day)
{
    switch (day)
    {
        case DayOfWeek.Saturday:
        case DayOfWeek.Sunday:
            return "Weekend";
        default:
            return "Weekday";
    }
}

The modern switch expression:

string GetDayType(DayOfWeek day) => day switch
{
    DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
    _ => "Weekday"
};

Notice how much cleaner this is. The _ is the discard pattern, which acts like default and matches anything. The or keyword is a logical pattern that combines two constant patterns. The compiler even warns you if your switch expression is not exhaustive — a huge win for correctness that the old statement never gave you.

Relational and Logical Patterns

C# 9.0 added relational patterns (<, >, <=, >=) and logical patterns (and, or, not). Together they let you express ranges and conditions naturally. This is where switch expressions really start to shine for real-world business logic:

string GetWaterState(int temperatureCelsius) => temperatureCelsius switch
{
    < 0 => "Ice",
    0 => "Freezing point",
    > 0 and < 100 => "Liquid",
    100 => "Boiling point",
    > 100 => "Steam"
};

Console.WriteLine(GetWaterState(-5));  // Ice
Console.WriteLine(GetWaterState(50));  // Liquid
Console.WriteLine(GetWaterState(150)); // Steam

Read that top to bottom and it is almost self-documenting. Compare it to the nested if/else if chain you would otherwise write and the readability benefit is obvious. The not pattern is equally handy for null checks: if (value is not null) reads far more clearly than if (value != null).

Property Patterns in C#

The property pattern lets you match on the properties of an object, not just its type. This is one of the most practical C# pattern matching examples for everyday development, because most of the objects we work with are complex types with meaningful properties. Suppose you are calculating shipping costs based on an order:

public record Order(decimal Total, string Country, bool IsPriority);

decimal CalculateShipping(Order order) => order switch
{
    { IsPriority: true }                     => 25.00m,
    { Total: > 100, Country: "USA" }         => 0.00m,   // free shipping
    { Country: "USA" }                       => 5.99m,
    { Country: "Canada" or "UK" }            => 12.99m,
    _                                        => 19.99m
};

Each { } block is a property pattern. You can nest as many properties as you like, and combine them with relational and logical patterns as shown with Total: > 100. The patterns are evaluated in order, so put your most specific cases first — a common pitfall is placing a broad pattern above a narrow one, which makes the narrow case unreachable (the compiler will warn you when it can prove this).

Nested and Extended Property Patterns

C# 10 introduced extended property patterns, which let you reach into nested properties using dot notation. Before C# 10 you had to nest braces; now you can flatten the access, which dramatically improves readability:

public record Address(string City, string Country);
public record Customer(string Name, Address Address);

string Classify(Customer customer) => customer switch
{
    // Extended property pattern (C# 10+)
    { Address.Country: "USA" } => "Domestic",
    { Address.Country: "Canada" or "Mexico" } => "North America",
    _ => "International"
};

You can also capture the matched value with a variable using the var pattern, which is useful when you need to use the value in the result: { Total: var total } when total > 1000 => .... The when clause adds an arbitrary boolean guard on top of the pattern for cases the pattern syntax alone cannot express.

C# List Patterns: Matching Sequences

One of the newest and most exciting features is C# list patterns, added in C# 11. List patterns let you match against arrays and lists based on their elements and length. This is perfect for parsing, validation, and algorithms that care about the structure of a sequence. The [ ] syntax describes the elements you expect:

int[] numbers = { 1, 2, 3 };

string Describe(int[] values) => values switch
{
    []          => "Empty",
    [var single] => $"One element: {single}",
    [var first, var second] => $"Two elements: {first} and {second}",
    [var head, .. var rest] => $"Starts with {head}, plus {rest.Length} more"
};

Console.WriteLine(Describe(numbers));          // Starts with 1, plus 2 more
Console.WriteLine(Describe(new int[0]));        // Empty
Console.WriteLine(Describe(new[] { 42 }));      // One element: 42

The star of list patterns is the slice pattern .., which matches zero or more elements. You can optionally capture the sliced elements with .. var rest. This makes it trivial to write head/tail style recursion or to validate a sequence's boundaries while ignoring the middle.

Practical List Pattern Example

List patterns shine when validating structured input, such as a simple command parser:

string RunCommand(string[] args) => args switch
{
    ["help"]                    => "Showing help...",
    ["add", var a, var b]       => $"Sum = {int.Parse(a) + int.Parse(b)}",
    ["delete", .. var files]    => $"Deleting {files.Length} file(s)",
    []                          => "No command provided",
    _                           => "Unknown command"
};

Console.WriteLine(RunCommand(new[] { "add", "5", "7" }));   // Sum = 12
Console.WriteLine(RunCommand(new[] { "delete", "a.txt", "b.txt" })); // Deleting 2 file(s)

This eliminates a great deal of manual index checking and length validation. Note that list patterns work on any type that has an indexer and a Length or Count property, so List<T>, arrays, and Span<T> all work seamlessly.

Pattern Matching Best Practices and Common Pitfalls

Now that you know the syntax, here are the pattern matching best practices that separate clean, maintainable code from a tangled mess.

  • Order matters — most specific first. Switch expressions and property patterns evaluate top to bottom. Put narrow patterns before broad ones, or the broad one will shadow the narrow one and the compiler will flag it as unreachable.
  • Make switches exhaustive. Always include a discard _ arm unless you have genuinely covered every case. An unhandled value in a switch expression throws a SwitchExpressionException at runtime — a discard arm turns that into predictable behavior.
  • Prefer switch expressions over statements when you are producing a value. They are more concise, they force exhaustiveness checking, and they avoid accidental fall-through bugs.
  • Use when guards sparingly. If you find yourself stacking many when clauses, the logic may belong in a dedicated method. Guards are an escape hatch, not the default tool.
  • Do not over-engineer. A simple two-branch check may be clearer as an if statement. Pattern matching earns its keep when there are several cases or when you are decomposing complex objects.

A frequent pitfall for beginners is forgetting that property patterns short-circuit on null. The pattern { Property: value } only matches when the object itself is non-null, which is convenient — but if you actually want to match null, use the constant pattern null explicitly. Another subtle trap is that comparing floating-point values with relational patterns inherits all the usual precision concerns of double and float, so be deliberate about boundary values.

Combining Patterns for Real Power

The true strength of C# pattern matching emerges when you combine type, property, relational, and list patterns in a single switch. Here is a richer example that decomposes a tuple and applies multiple pattern types at once:

static string ClassifyPoint(int x, int y) => (x, y) switch
{
    (0, 0)                 => "Origin",
    (var px, 0)            => $"On X-axis at {px}",
    (0, var py)            => $"On Y-axis at {py}",
    (var px, var py) when px == py => "On diagonal",
    _                      => "Somewhere else"
};

This positional pattern deconstructs the tuple directly in the switch. The same technique works with any type that has a Deconstruct method — including records, which generate one automatically — making pattern matching a natural fit for modern, record-heavy C# codebases.

Conclusion: Key Takeaways

You now have a complete understanding of C# pattern matching, from the foundational type patterns to cutting-edge list patterns. Used well, these features make your code shorter, safer, and far easier to read. Here are the key takeaways to remember:

  • Switch expressions return values, require no break, and give you compiler-enforced exhaustiveness checking.
  • Property patterns let you match and destructure objects by their members, and extended property patterns flatten nested access.
  • List patterns (C# 11) elegantly match sequences using [ ] and the slice pattern ...
  • Relational and logical patterns (<, >, and, or, not) express ranges and conditions declaratively.
  • Follow best practices: order patterns most-specific-first, keep switches exhaustive, and reach for pattern matching when it genuinely improves clarity.

The best way to master C# pattern matching is to practice. Take an old if/else chain in your codebase and refactor it into a switch expression today — you will immediately feel the difference. Bookmark this guide, experiment with the runnable examples above, and you will be writing cleaner, more expressive C# in no time. Happy coding!

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...