Skip to main content

C# Pattern Matching: Complete Guide with Examples (2026)

Learn C# pattern matching with this complete guide covering switch expressions, property patterns & list patterns. Runnable examples & best practices — start now!

C# pattern matching is one of the most powerful features added to the language in recent years, and it fundamentally changes how you write conditional logic. Instead of long, nested if-else chains and clunky type checks, C# pattern matching lets you test the shape, type, and values of data in a single, readable expression. In this complete guide, you'll learn everything from basic type patterns to advanced switch expressions, property patterns, and list patterns — with runnable C# code examples you can drop straight into your projects.

Whether you're a beginner searching for how to use the switch statement, an intermediate developer looking for best practices, or a senior engineer exploring advanced pattern matching in C#, this tutorial covers it all. Let's dig in.

What Is C# Pattern Matching?

At its core, pattern matching in C# is a way to test whether a value matches a certain "pattern" — and, if it does, to extract data from it in the same step. It was introduced in C# 7.0 and has been expanded in nearly every release since, with C# 8, 9, 10, and 11 adding switch expressions, property patterns, relational patterns, and list patterns.

Why does this matter? Traditional conditional code is verbose and error-prone. Pattern matching makes your intent explicit, reduces boilerplate, and — critically — enables the compiler to catch missing cases. The result is code that is safer, shorter, and easier to maintain.

Here's the classic "before" example that developers write every day:

object value = "Hello";

if (value is string)
{
    string text = (string)value;
    Console.WriteLine($"String of length {text.Length}");
}

With C# pattern matching, this collapses into a single, safe line using the type pattern:

object value = "Hello";

if (value is string text)
{
    Console.WriteLine($"String of length {text.Length}");
}

The is keyword now both checks the type and assigns the result to a new variable text if the check succeeds. No cast, no null issues, no repetition.

The Type Pattern and the Declaration Pattern

The most common starting point for pattern matching is the type pattern combined with a declaration. This is especially useful when working with polymorphism or object types.

public static string Describe(object shape)
{
    if (shape is Circle c)
        return $"Circle with radius {c.Radius}";

    if (shape is Rectangle r)
        return $"Rectangle {r.Width} x {r.Height}";

    return "Unknown shape";
}

Notice how the variables c and r are only in scope where the pattern matched. This scoping prevents accidental use of the wrong variable — a subtle but valuable safety feature.

Switch Expressions in C#

One of the biggest wins from C# pattern matching is the switch expression, introduced in C# 8.0. Unlike the traditional switch statement, a switch expression returns a value, uses concise arrow syntax, and eliminates break statements entirely.

Here's the traditional switch statement:

public static string GetSeason(int month)
{
    switch (month)
    {
        case 12:
        case 1:
        case 2:
            return "Winter";
        case 3:
        case 4:
        case 5:
            return "Spring";
        default:
            return "Other";
    }
}

And here is the same logic rewritten as a modern switch expression:

public static string GetSeason(int month) => month switch
{
    12 or 1 or 2 => "Winter",
    3 or 4 or 5  => "Spring",
    6 or 7 or 8  => "Summer",
    9 or 10 or 11 => "Autumn",
    _ => throw new ArgumentOutOfRangeException(nameof(month))
};

The _ is the discard pattern, which acts like the default case. The or keyword combines patterns. This is dramatically more readable, and the compiler will warn you if your patterns aren't exhaustive — a huge advantage for correctness.

Relational and Logical Patterns

C# 9.0 added relational patterns (<, >, <=, >=) and logical patterns (and, or, not). These make range checks beautifully expressive:

public static string ClassifyTemperature(int celsius) => celsius switch
{
    < 0            => "Freezing",
    >= 0 and < 15  => "Cold",
    >= 15 and < 25 => "Mild",
    >= 25 and < 35 => "Warm",
    _              => "Hot"
};

Compare that to a chain of if-else if statements — the pattern-based version reads almost like a specification. The not pattern is also perfect for null checks: if (value is not null) is clearer than if (value != null).

Property Patterns in C#

The property pattern lets you match against the properties of an object. This is where C# pattern matching starts to feel genuinely powerful, because you can inspect an object's internal state without writing multiple nested conditions.

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

public static decimal CalculateShipping(Order order) => order switch
{
    { Total: > 100, Country: "USA" } => 0m,          // Free shipping over $100
    { IsExpress: true }              => 25m,
    { Country: "USA" }               => 5m,
    { Country: "UK" or "CA" or "AU" } => 15m,
    _                                => 20m
};

You can also nest property patterns to reach deep into object graphs, and combine them with relational and logical patterns. For example, matching a nested address:

if (order is { Total: > 500, Customer.Address.Country: "USA" } premium)
{
    Console.WriteLine("Eligible for premium concierge service");
}

That single line replaces four or five null checks and property accesses. This is a common reason developers search for "C# property pattern" — it eliminates the dreaded pyramid of nested if statements.

Positional Patterns with Deconstruction

If your type supports deconstruction (records do this automatically), you can use positional patterns to match by position:

public record Point(int X, int Y);

public static string Locate(Point point) => point switch
{
    (0, 0)           => "Origin",
    (var x, 0)       => $"On X-axis at {x}",
    (0, var y)       => $"On Y-axis at {y}",
    (var x, var y)   => $"Point at ({x}, {y})"
};

The var pattern always matches and captures the value into a new variable, which you can then use in the result expression or a guard clause.

List Patterns in C# (C# 11)

List patterns are one of the newest and most requested features, added in C# 11. They allow you to match arrays and lists against a sequence of element patterns. This is a game-changer for parsing, validation, and functional-style code.

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

string result = numbers switch
{
    []           => "Empty",
    [var single] => $"One element: {single}",
    [var first, _, var last] => $"First {first}, last {last}",
    _            => "Many elements"
};

The slice pattern (..) matches zero or more elements, and can optionally capture them:

int[] data = { 10, 20, 30, 40, 50 };

if (data is [var head, .. var middle, var tail])
{
    Console.WriteLine($"Head: {head}, Tail: {tail}, Middle count: {middle.Length}");
    // Head: 10, Tail: 50, Middle count: 3
}

List patterns shine when validating command input or parsing structured data:

string[] command = { "move", "north", "3" };

string action = command switch
{
    ["quit"]                      => "Exiting game",
    ["move", var direction, var steps] => $"Moving {direction} by {steps}",
    ["help", ..]                  => "Showing help",
    _                             => "Unknown command"
};

Guard Clauses with the when Keyword

Sometimes a pattern alone isn't enough — you need an extra condition. The when keyword adds a guard clause to any case:

public static string Grade(int score) => score switch
{
    var s when s >= 90 => "A",
    var s when s >= 80 => "B",
    var s when s >= 70 => "C",
    _                  => "F"
};

Guards let you combine pattern matching with arbitrary boolean logic, giving you the best of both worlds.

Best Practices for C# Pattern Matching

  • Prefer switch expressions over switch statements when you're returning a value. They're more concise and the compiler enforces exhaustiveness.
  • Always handle the discard case (_) to avoid runtime SwitchExpressionException. If a value doesn't match any arm, C# throws at runtime — so cover every case or throw a meaningful exception explicitly.
  • Use is not null instead of != null for clarity, and because it can't be overridden by a custom != operator.
  • Keep patterns readable — deeply nested property patterns can become hard to follow. If a pattern spans multiple lines and conditions, consider extracting a well-named method.
  • Order patterns from most specific to most general. The first matching arm wins, so a broad pattern placed too early will shadow more specific ones — and the compiler will often warn you.

Common Pitfalls to Avoid

  • Non-exhaustive switch expressions: Forgetting the _ arm compiles with a warning but can throw at runtime. Treat that warning seriously.
  • Unreachable patterns: Placing _ or a broad pattern before specific ones makes later arms unreachable. The compiler flags this as an error in switch expressions.
  • Overusing pattern matching: Not every conditional needs a switch expression. A simple boolean check is often clearer than a two-arm switch.
  • Mutating state in guard clauses: Keep when guards side-effect free. Guards can be evaluated in ways that surprise you, and side effects make debugging painful.
  • Ignoring null in type patterns: Remember that value is string returns false for null. Handle null explicitly with a null pattern when it matters.

Putting It All Together

Here's a real-world example combining type patterns, property patterns, relational patterns, and guards to process different event types in a system — the kind of clean, declarative code that C# pattern matching enables:

public static string HandleEvent(object evt) => evt switch
{
    LoginEvent { FailedAttempts: > 3 } => "Account locked",
    LoginEvent { IsSuccessful: true } => "Welcome back",
    PurchaseEvent { Amount: > 1000 } p => $"High-value order: {p.Amount:C}",
    PurchaseEvent p => $"Order received: {p.Amount:C}",
    null => "No event",
    _ => "Unhandled event type"
};

Conclusion: Key Takeaways

C# pattern matching transforms verbose, error-prone conditional code into concise, expressive, and safe logic. In this complete guide, you learned how to use switch expressions, property patterns, positional patterns, relational and logical patterns, list patterns, and guard clauses — all with practical, runnable examples.

Here are the key takeaways to remember:

  • Use switch expressions instead of switch statements when returning a value for cleaner, exhaustive code.
  • Property patterns eliminate nested if checks and let you match on object state directly.
  • List patterns (C# 11) make parsing and validating sequences elegant and readable.
  • Combine patterns with and, or, not, and when guards for maximum expressiveness.
  • Always cover the discard case _ to avoid runtime exceptions.

The best way to master pattern matching in C# is to practice. Take an existing block of nested if-else logic in your own codebase and refactor it into a switch expression — you'll immediately see how much cleaner and more maintainable your code becomes. 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

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

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...