Pattern Matching
Pattern matching lets you test a value against a shape or condition and, if it matches, extract data from it — all in a single expression, rather than a series of separate type checks and casts. Starting with the is expression in C# 7 and expanding significantly through C# 8-11, C# now supports type patterns, constant patterns, property patterns, positional (deconstruction) patterns, relational patterns, and combinators (and, or, not) that can be composed together. This turns what used to be verbose chains of if/else if with casts into declarative, exhaustively-checkable expressions.
Cricket analogy: Old-style dismissal checking meant separate 'is it bowled? caught? lbw?' checks with casts; pattern matching is like a single review that tests shape and extracts the outcome (type, constant, property, positional patterns) at once, combining 'caught and not no-ball' with and/or/not.
Type patterns and the is expression
The simplest form, x is Foo f, tests whether x is of type Foo (or a derived type) and, if so, assigns it to a new variable f scoped to the surrounding code — this pattern variable is definitely-assigned only where the compiler can prove the check succeeded, such as inside the true-branch of an if or after a guard clause with a negated check followed by a return/continue.
Cricket analogy: 'delivery is NoBall nb' is like an umpire checking if a delivery counts as a no-ball and, only if so, handing the scorer a labeled note nb to record the free hit - that note only exists in the branch where the check passed.
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Triangle(double Base, double Height) : Shape;
public static class ShapeCalculator
{
public static double Area(Shape shape) => shape switch
{
Circle { Radius: <= 0 } => throw new ArgumentException("Radius must be positive"),
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle { Width: var w, Height: var h } => w * h,
Triangle(var b, var h) => 0.5 * b * h,
null => throw new ArgumentNullException(nameof(shape)),
_ => throw new NotSupportedException($"Unknown shape: {shape.GetType().Name}")
};
public static string Classify(Shape shape) => shape switch
{
Circle or Rectangle { Width: var w, Height: var h } and not { Width: var w2 } when w == h => "regular",
_ => "irregular"
};
}Property, positional, and relational patterns
Property patterns ({ Radius: <= 0 }) match against one or more properties of an object, optionally nesting further patterns inside. Positional patterns (Triangle(var b, var h)) rely on a type's Deconstruct method — automatically synthesized for records — to match and bind multiple values at once. Relational patterns (<= 0, > 100) test numeric or comparable values directly inside a pattern, and logical pattern combinators and, or, and not let you compose these building blocks, e.g. >= 0 and <= 100 for a range check.
Cricket analogy: '{ Overs: <= 0 }' flags an invalid spell length; a positional pattern like Partnership(var runs, var balls) deconstructs a stand into components, while a relational check '>= 0 and <= 6' validates a legal runs-per-ball range.
switch expressions vs switch statements
A switch expression (introduced C# 8) evaluates to a value using => arms and no break statements, is more concise, and — crucially — the compiler warns if it isn't exhaustive over a closed type hierarchy (like a sealed record hierarchy), catching missing cases at compile time. A traditional switch statement remains useful for executing multiple statements per case or when no single result value is being produced.
Cricket analogy: A switch expression 'dismissal switch { Bowled => ..., Caught => ..., Lbw => ... }' evaluates to a value with no break statements, and the compiler warns if a sealed dismissal hierarchy is missing a case; a traditional switch is better for logging multiple actions per dismissal.
Pattern matching in C# is intentionally similar in spirit to pattern matching in functional languages like F# or Rust's match, bringing some of that exhaustiveness-checking and destructuring ergonomics to an object-oriented language — records with positional deconstruction make this especially natural since C# 9.
A switch expression without a final _ discard pattern (or that isn't provably exhaustive) throws a runtime InvalidOperationException if no arm matches — the compiler only warns about non-exhaustiveness, it doesn't prevent compilation, so an unhandled case can still surface as a runtime crash rather than a build error.
isexpressions and switch expressions/statements support type, constant, property, positional, and relational patterns.- Property patterns like
{ Radius: <= 0 }match and can nest against object properties. - Positional patterns rely on Deconstruct, which records synthesize automatically.
- Logical pattern combinators
and,or,notcompose simpler patterns into richer conditions. - switch expressions are exhaustiveness-checked by the compiler (as warnings) over closed hierarchies, unlike switch statements.
- A non-exhaustive switch expression throws InvalidOperationException at runtime if no pattern matches.
Practice what you learned
1. What does the pattern `{ Radius: <= 0 }` test for?
2. What enables a type to be used with a positional pattern like `Triangle(var b, var h)`?
3. What is a key advantage of a switch expression over a traditional switch statement?
4. What happens at runtime if a switch expression has no matching arm and no discard pattern?
5. Which combinator would you use to express 'value is between 0 and 100 inclusive' as a pattern?
Was this page helpful?
You May Also Like
Switch Statements and Expressions
Learn how C# branches on a value using the classic switch statement and the modern, expression-based switch introduced in C# 8, including pattern matching arms.
Nullable Reference Types
See how C# 8's nullable reference types add compile-time null-safety annotations on top of the runtime's existing nullable value type system.
Polymorphism Explained
See how C# achieves polymorphism through virtual method dispatch, interface implementations, and method/operator overloading, letting one code path handle many shapes of data.
C# Quick Reference
A condensed cheat sheet of core C# syntax, keywords, and idioms — data types, control flow, collections, and modern language features at a glance.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics