Pattern Matching in C#

Vaibhav • September 11, 2025

Pattern matching is one of the most expressive and modern features in C#. It allows you to write cleaner, more readable code by combining type checks, conditionals, and destructuring into a single, elegant syntax. Instead of writing verbose if and switch statements, pattern matching lets you describe the shape and properties of data directly in your logic.

In this article, we’ll explore how pattern matching works, how it builds on concepts you already know (like switch and is), and how to use it effectively in real-world scenarios. We’ll also cover common patterns, best practices, and subtle language rules that help you avoid bugs and write expressive code.

What is Pattern Matching?

Pattern matching is a way to test whether a value fits a certain shape or condition, and then extract information from it. It’s not just about checking types - it’s about expressing intent clearly. You’ve already seen basic type checks using is:

object value = "hello";

if (value is string s)
{
    Console.WriteLine($"Length: {s.Length}");
}

This is a simple pattern: it checks if value is a string, and if so, assigns it to s. This avoids the need for casting and makes your code safer and clearer.

Pattern matching works with reference types, value types, and even custom types. It’s deeply integrated into the language and works with switch, is, and when clauses.

Type Patterns

The most basic pattern is the type pattern. It checks whether a value is of a certain type and, if so, assigns it to a new variable:

object data = 42;

if (data is int number)
{
    Console.WriteLine(number * 2); // Output: 84
}

This replaces the old way of checking and casting:

if (data is int)
{
    int number = (int)data;
    Console.WriteLine(number * 2);
}

With pattern matching, you get both the check and the assignment in one step. It’s safer and more concise.

Constant Patterns

You can match against constant values directly. This is especially useful in switch expressions:

char grade = 'B';

switch (grade)
{
    case 'A':
        Console.WriteLine("Excellent");
        break;
    case 'B':
        Console.WriteLine("Good");
        break;
    case 'C':
        Console.WriteLine("Average");
        break;
    default:
        Console.WriteLine("Unknown");
        break;
}

This is a constant pattern - it matches a specific value. You’ve used this before, but now it’s part of a broader pattern matching system.

Relational Patterns

Relational patterns let you match values based on comparisons:

int score = 85;

if (score is >= 90)
    Console.WriteLine("Outstanding");
else if (score is >= 75 and < 90)
    Console.WriteLine("Very Good");
else
    Console.WriteLine("Needs Improvement");

This is much cleaner than writing multiple if conditions. You can combine relational patterns with logical patterns like and, or, and not.

Note: Relational patterns work with numeric types and characters. They’re evaluated left to right, and you can combine them with other patterns for more expressive logic.

Logical Patterns

Logical patterns let you combine multiple conditions. For example:

int age = 25;

if (age is >= 18 and < 65)
{
    Console.WriteLine("Working age");
}

You can also use or and not:

if (age is < 18 or >= 65)
{
    Console.WriteLine("Not working age");
}

These patterns make your intent clear and reduce the need for nested if statements.

Property Patterns

Property patterns let you match on the values of properties inside an object. Suppose you have a class:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

You can match on properties like this:

Person p = new Person { Name = "Vaibhav", Age = 30 };

if (p is { Age: >= 18 })
{
    Console.WriteLine($"{p.Name} is an adult.");
}

This checks the Age property directly. You can match multiple properties too:

if (p is { Name: "Vaibhav", Age: >= 30 })
{
    Console.WriteLine("Match found");
}

Property patterns work with auto-properties, fields, and even nested objects. You can match deeply structured data with a single expression.

Switch Expressions with Patterns

C# supports switch expressions, which are concise and expressive. They work beautifully with pattern matching:

string GetGradeMessage(char grade) => grade switch
{
    'A' => "Excellent",
    'B' => "Good",
    'C' => "Average",
    _   => "Unknown"
};

You can use relational and logical patterns here too:

string GetAgeGroup(int age) => age switch
{
    < 13 => "Child",
    >= 13 and < 18 => "Teen",
    >= 18 and < 65 => "Adult",
    _ => "Senior"
};

This is much cleaner than traditional switch statements and avoids fall-through bugs.

Positional Patterns

Positional patterns work with types that support Deconstruct. Suppose you have a tuple:

(int x, int y) point = (3, 4);

if (point is (0, 0))
    Console.WriteLine("Origin");
else if (point is (var a, var b))
    Console.WriteLine($"Point at ({a}, {b})");

You can use positional patterns with custom types too, if they implement a Deconstruct method.

Discard Patterns

Sometimes you want to match a shape but ignore certain values. Use the discard pattern _:

if (point is (_, 0))
    Console.WriteLine("On X-axis");

This matches any value for x as long as y is 0.

Combining Patterns

You can combine patterns to express complex logic. For example:

object input = 42;

if (input is int i and >= 0 and <= 100)
{
    Console.WriteLine($"Valid score: {i}");
}

This checks the type, range, and assigns the value - all in one line.

Pattern Matching with Null

Pattern matching works with null too. You can check for null explicitly:

string? name = null;

if (name is null)
    Console.WriteLine("Name is missing");

This is clearer than using == null and works consistently with nullable reference types.

Common Mistakes and How to Avoid Them

Pattern matching is powerful, but there are a few traps to avoid:

  • Don’t use is without a variable - you lose the benefit of assignment.
  • Be careful with overlapping patterns - the first match wins.
  • Use _ as a fallback to avoid missing cases.
  • Prefer switch expressions for clarity when returning values.

Use pattern matching to express intent clearly. Avoid deeply nested if statements - combine patterns with and, or, and not for readable logic.

Summary

Pattern matching is a modern, expressive feature in C# that helps you write cleaner, safer, and more readable code. You’ve learned how to use type patterns, constant patterns, relational and logical patterns, property and positional patterns, and how to combine them effectively. You’ve also seen how pattern matching integrates with switch expressions and nullable types.

By using pattern matching, you reduce boilerplate, clarify your logic, and make your code easier to maintain. In the next article, we’ll explore Record Types - a concise way to define immutable data structures with built-in value equality and pattern matching support.