C# · Intermediate

Pattern Matching

Pattern matching lets you ask “what shape is this data?” and act on the answer in one readable switch. It replaces long chains of if, casts and null checks.

Advanced 6 min Lesson 24 of 26Examples target C# 13 on .NET 9 unless stated otherwise.
0/26 in C#
On this page · 6 min · Advanced

In simple words#

A sorting machine at a post office checks each parcel’s shape and label and drops it into the right bin. Pattern matching checks a value’s type or shape and picks the matching branch.

is
Check a type or pattern.
switch expression
Compact switch that returns a value.
Property pattern
{ Age: >= 18 } matches by property values.

Learning objectives#

  • Test and cast in one step with is
  • Use type, relational and logical patterns
  • Match on properties and positions
  • Add conditions with when and match lists

Explanation#

is combines a type check and a cast: if (obj is string s) uses s as a string inside the if.

Relational patterns compare: > 0, <= 100. Logical patterns combine them: >= 18 and < 60, not null, "Sat" or "Sun".

Property patterns look inside objects: order is { Country: "IN", Total: >= 1000 }. Positional patterns deconstruct records: Rect(var w, var h).

A when clause adds any extra condition to a case: int n when n % 2 == 0.

List patterns (C# 11) match arrays and lists: [1, ..] means “starts with 1”, [] means empty.

Switch expressions try the arms from top to bottom, and the compiler warns if a case is missing or can never match.

Syntax#

C# Illustrative
string label = input switch
{
    null => "missing",
    int n when n < 0 => "negative",
    int n => 
quot;number
{n}",
string { Length: 0 } => "empty text", _ => "other" };

Code example: Describing anything, pricing shipping#

C#Program.cs Illustrative
object?[] items = [42, "hello", 3.5, null, new Circle(2), new Rect(3, 3)];
foreach (var item in items)
{
    Console.WriteLine(Describe(item));
}


Console.WriteLine(Shipping(new Order(1200m, "IN", IsPrime: false)));
Console.WriteLine(Shipping(new Order(300m, "IN", IsPrime: false)));
Console.WriteLine(Shipping(new Order(300m, "US", IsPrime: true)));
Console.WriteLine(Shipping(new Order(300m, "US", IsPrime: false)));


int[] numbers = [1, 2, 3];
Console.WriteLine(numbers is [1, ..]);


static string Describe(object? o) => o switch
{
    null => "nothing",
    int n when n > 0 => 
quot;positive int
{n}",
int n =>
quot;int
{n}",
string s =>
quot;text of length
{s.Length}",
Circle { Radius: > 1 } c =>
quot;big circle r=
{c.Radius}",
Rect(var w, var h) when w == h =>
quot;square
{w}x{h}",
_ =>
quot;something else:
{o}"
}; static decimal Shipping(Order order) => order switch { { IsPrime: true } => 0m, { Country: "IN", Total: >= 1000m } => 0m, { Country: "IN" } => 49m, _ => 999m }; record Circle(double Radius); record Rect(double W, double H); record Order(decimal Total, string Country, bool IsPrime);
Expected output
positive int 42
text of length 5
something else: 3.5
nothing
big circle r=2
square 3x3
0
49
0
999
True

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Describe checks null first, then types. int n when n > 0 adds a guard.
  2. Circle { Radius: > 1 } c matches circles with a radius above 1 and names the result c.
  3. Rect(var w, var h) deconstructs the record into w and h; the when guard keeps only squares.
  4. 3.5 is a double, which no arm handles, so the discard _ catches it.
  5. Shipping reads like a pricing table: Prime ships free, big Indian orders ship free, other Indian orders cost 49, everything else 999.
  6. numbers is [1, ..] is a list pattern: “starts with 1, anything after”.

Real-world use case#

An API maps domain results to HTTP responses with one switch: result switch { NotFound => NotFound(), ValidationFailed v => BadRequest(v.Errors), Success s => Ok(s.Value) }.

Common mistakes#

Wrong arm order

int n => ... above int n when n > 0 => ... makes the second arm unreachable; the compiler reports error CS8510.

Missing the default arm

Without _ the switch expression throws SwitchExpressionException for unhandled input. The compiler warns you — do not ignore it.

Using is with a cast afterwards

if (o is string) { var s = (string)o; } does the check twice. Use if (o is string s).

Best practices#

  • Put specific patterns before general ones
  • Always handle null and _ explicitly
  • Prefer is not null over != null
  • Keep each arm short; call a method for complex work

Remember this#

is Type name = check + cast in one go.

and / or / not combine patterns.

{ Prop: value } looks inside an object.

Practice exercise#

Your task: Write a switch expression that returns a ticket price by age: under 5 → 0, 5–17 → 100, 60 and over → 120, everyone else → 200.

Need a hint?
Use relational and logical patterns: < 5, >= 5 and < 18, >= 60, _.
Show solution
C# Illustrative
foreach (int age in new[] { 3, 12, 35, 70 })
{
    Console.WriteLine(
quot;
{age}: {TicketPrice(age)}");
} // 3: 0, 12: 100, 35: 200, 70: 120 static int TicketPrice(int age) => age switch { < 5 => 0, >= 5 and < 18 => 100, >= 60 => 120, _ => 200 };

Mini quiz#

Pattern Matching quizPredict the output Question 1 of 3

What is printed?

int t = 35;
Console.WriteLine(t switch { < 15 => "Cold", >= 15 and < 30 => "Nice", _ => "Hot" });
Score so far: 0/0

Want more? Practise all C# MCQs · C# interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.