
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 aSwitchExpressionExceptionat 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
whenguards sparingly. If you find yourself stacking manywhenclauses, 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
ifstatement. 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!
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