
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 runtimeSwitchExpressionException. 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 nullinstead of!= nullfor 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
whenguards 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 stringreturnsfalsefornull. Handle null explicitly with anullpattern 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
ifchecks 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, andwhenguards 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!
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment