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.