Pattern matching lets you test a value against a shape (a type, a set of properties, a range, a sequence) and extract data from it in one step. It has grown from a simple type-check shorthand in C# 7.0 into a comprehensive system for expressing complex conditional logic declaratively.
The basics
A pattern appears wherever the language expects a test: after is in an expression, in the arms of a switch expression, or in the cases of a switch statement. The compiler checks the value against the pattern and, if it matches, optionally binds variables you can use in the body.
// Type pattern: test and cast in one step
if (shape is Circle c)
Console.WriteLine($"Radius: {c.Radius}");
// Constant pattern: match a literal value
if (statusCode is 404)
Console.WriteLine("Not found");
// var pattern: always matches, captures the value
if (GetResult() is var result && result > 0)
Console.WriteLine(result);Switch expressions
Switch expressions C# 8.0 provide a concise way to produce a value from a set of patterns. Each arm maps a pattern to a result, and the discard _ serves as the default.
string Describe(object obj) => obj switch
{
int n when n < 0 => "negative integer",
int n => $"integer: {n}",
string s => $"string of length {s.Length}",
null => "null",
_ => "something else"
};Traditional switch statements also support patterns, which is useful when each case needs to execute multiple statements rather than return a value.
Pattern kinds
The table below lists every pattern kind and the version it was introduced. They can be freely combined: a property pattern can contain relational patterns, a list pattern can contain type patterns, and so on.
| Pattern | Since | Example |
|---|---|---|
| Type | 7.0 | is string s |
| Constant | 7.0 | is 42, is null |
| Var | 7.0 | is var x |
| Discard | 7.0 | _ |
| Generic type | 7.1 | is T t (in generic methods) |
| Positional | 8.0 | is (int x, int y) |
| Property | 8.0 | is { Length: > 0 } |
| Tuple | 8.0 | (a, b) switch { (true, true) => ... } |
| Relational | 9.0 | is > 0 and < 100 |
Logical (and, or, not) | 9.0 | is not null |
| Simplified type | 9.0 | is int (without variable) |
| Extended property | 10.0 | is { Address.City: "London" } |
| List | 11.0 | is [1, .., > 0] |
| Span on string | 11.0 | span is "hello" |
Combining patterns
Patterns compose naturally. The logical keywords and, or, and not (C# 9.0) let you build complex conditions that remain readable.
string ClassifyTemperature(double temp) => temp switch
{
< 0 => "freezing",
>= 0 and < 15 => "cold",
>= 15 and < 25 => "comfortable",
>= 25 and < 35 => "warm",
>= 35 => "hot"
};
bool IsValidInput(object obj) => obj is not null and not "";Property and positional patterns
Property patterns C# 8.0 match against an object's properties. Extended property patterns C# 10.0 allow dot-notation for nested access.
// Property pattern
if (order is { Status: "shipped", Items.Count: > 0 })
Notify(order);
// Positional pattern, works with types that have Deconstruct
if (point is (0, 0))
Console.WriteLine("Origin");Positional patterns work with any type that has a Deconstruct method, including tuples and records C# 9.0.
Tuple patterns
Tuple patterns C# 8.0 let you match on multiple values at once without nesting if statements. This is especially useful for state machines.
string RockPaperScissors(string a, string b) => (a, b) switch
{
("rock", "scissors") => "a wins",
("scissors", "paper") => "a wins",
("paper", "rock") => "a wins",
(_, _) when a == b => "tie",
_ => "b wins"
};List patterns
List patterns C# 11.0 match sequences by position. The slice pattern .. matches zero or more elements and can capture them into a variable.
string Describe(int[] arr) => arr switch
{
[] => "empty",
[var only] => $"single: {only}",
[0, ..] => "starts with zero",
[.., < 0] => "ends negative",
[_, .. var mid, _] => $"middle has {mid.Length} elements"
};List patterns work with any type that is countable and indexable, including arrays, List<T>, and Span<T>.
Guards
Any pattern arm can add a when clause for conditions that cannot be expressed as a pattern.
string Categorize(int[] numbers) => numbers switch
{
[var first, ..] when first == numbers[^1] => "first equals last",
{ Length: > 100 } => "large collection",
_ => "other"
};Tips
- Exhaustiveness. The compiler warns when a
switchexpression does not cover all possible inputs. Add a discard_arm or ensure all cases are handled. - Order matters. Arms are evaluated top to bottom. Place more specific patterns before more general ones, or the compiler will flag unreachable arms.
- Performance. The compiler optimizes pattern matching into efficient branching. Prefer patterns over hand-written
if/elsechains; they are usually both clearer and faster. - Null. The constant pattern
nulland thenot nullpattern are the idiomatic way to handle null checks in pattern-matching code. A type pattern likeis string sdoes not match null.