C# Pattern Matching
Pattern matching lets you test a value against a shape, type, or condition and extract information from it — all in one concise expression. It makes complex conditional code shorter, cleaner, and easier to read.
Why Pattern Matching?
┌──────────────────────────────────────────────────────────────┐
│ WITHOUT pattern matching: │
│ │
│ if (obj is string) │
│ { │
│ string s = (string)obj; // cast separately │
│ Console.WriteLine(s.Length); │
│ } │
│ │
│ WITH pattern matching: │
│ │
│ if (obj is string s) // test AND cast in one step │
│ { │
│ Console.WriteLine(s.Length); │
│ } │
└──────────────────────────────────────────────────────────────┘
Type Pattern — is keyword
The is keyword checks the type and, if matched, assigns the value to a new variable in one step.
object[] items = { "hello", 42, 3.14, true, "world" };
foreach (object item in items)
{
if (item is string text)
Console.WriteLine("String: " + text.ToUpper());
else if (item is int number)
Console.WriteLine("Int: " + (number * 2));
else if (item is double d)
Console.WriteLine("Double: " + d);
else if (item is bool flag)
Console.WriteLine("Bool: " + flag);
}
// String: HELLO
// Int: 84
// Double: 3.14
// Bool: True
// String: WORLD
Switch Expression with Patterns (C# 8+)
Switch expressions return a value based on pattern matching. They replace verbose switch statements with a concise arrow syntax.
static string Classify(object obj) => obj switch
{
int n when n < 0 => "Negative integer",
int n when n == 0 => "Zero",
int n => "Positive integer: " + n,
string s => "String of length " + s.Length,
bool b => "Boolean: " + b,
null => "Null value",
_ => "Unknown type" // _ = default case
};
Console.WriteLine(Classify(-5)); // Negative integer
Console.WriteLine(Classify(0)); // Zero
Console.WriteLine(Classify(42)); // Positive integer: 42
Console.WriteLine(Classify("hello")); // String of length 5
Console.WriteLine(Classify(null)); // Null value
Console.WriteLine(Classify(3.14)); // Unknown type
Switch Expression Diagram
┌──────────────────────────────────────────────────────────────┐
│ obj switch │
│ { │
│ pattern1 => result1, ← checked first │
│ pattern2 => result2, ← checked second │
│ pattern3 => result3, ← checked third │
│ _ => default ← fallback if none match │
│ } │
│ │
│ Returns the value of the first matching arm │
└──────────────────────────────────────────────────────────────┘
Property Pattern
Property patterns match based on the values of an object's properties — no need to extract values manually first.
class Order
{
public string Status { get; set; }
public double Total { get; set; }
}
static string GetLabel(Order o) => o switch
{
{ Status: "Cancelled" } => "❌ Cancelled",
{ Status: "Shipped", Total: > 1000 } => "🚀 Priority Shipment",
{ Status: "Shipped" } => "📦 Shipped",
{ Status: "Pending", Total: > 500 } => "⏳ Large Pending",
{ Status: "Pending" } => "⏳ Pending",
_ => "❓ Unknown"
};
Console.WriteLine(GetLabel(new Order { Status = "Shipped", Total = 1500 }));
// 🚀 Priority Shipment
Console.WriteLine(GetLabel(new Order { Status = "Pending", Total = 200 }));
// ⏳ Pending
Console.WriteLine(GetLabel(new Order { Status = "Cancelled", Total = 50 }));
// ❌ Cancelled
Relational Patterns (C# 9+)
Relational patterns use <, >, <=, >= directly in switch arms.
static string GetGrade(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
Console.WriteLine(GetGrade(95)); // A
Console.WriteLine(GetGrade(82)); // B
Console.WriteLine(GetGrade(55)); // F
Logical Patterns: and, or, not (C# 9+)
Patterns can be combined using and, or, and not.
static string Classify(int n) => n switch
{
< 0 => "Negative",
0 => "Zero",
> 0 and <= 10 => "Small positive",
> 10 and <= 100 => "Medium positive",
> 100 => "Large positive"
};
Console.WriteLine(Classify(-3)); // Negative
Console.WriteLine(Classify(0)); // Zero
Console.WriteLine(Classify(7)); // Small positive
Console.WriteLine(Classify(50)); // Medium positive
Console.WriteLine(Classify(999)); // Large positive
// 'not' pattern:
bool IsNotNull(object obj) => obj is not null;
Console.WriteLine(IsNotNull("hello")); // True
Console.WriteLine(IsNotNull(null)); // False
Tuple Pattern
Tuple patterns match multiple values at once inside a switch expression.
static string GetTrafficLight(string color, bool isDay) => (color, isDay) switch
{
("Red", _ ) => "Stop",
("Green", true ) => "Go — daytime",
("Green", false) => "Go — but caution at night",
("Yellow", _ ) => "Slow down",
_ => "Unknown signal"
};
Console.WriteLine(GetTrafficLight("Red", true)); // Stop
Console.WriteLine(GetTrafficLight("Green", false)); // Go — but caution at night
Console.WriteLine(GetTrafficLight("Yellow", true)); // Slow down
Tuple Pattern Diagram
┌──────────────────────────────────────────────────────────────┐
│ (color, isDay) switch │
│ │
│ ("Red", _ ) → match Red, any day value │
│ ("Green", true ) → match Green AND daytime │
│ ("Green", false) → match Green AND nighttime │
│ _ → match everything else │
└──────────────────────────────────────────────────────────────┘
List Pattern (C# 11+)
List patterns match elements at specific positions in an array or list.
int[] arr1 = { 1, 2, 3 };
int[] arr2 = { 1, 2, 99 };
int[] arr3 = { 1 };
int[] arr4 = { };
static string Describe(int[] arr) => arr switch
{
[] => "Empty array",
[var x] => "One element: " + x,
[1, 2, 3] => "Exactly 1, 2, 3",
[1, 2, ..] => "Starts with 1, 2",
_ => "Other"
};
Console.WriteLine(Describe(arr1)); // Exactly 1, 2, 3
Console.WriteLine(Describe(arr2)); // Starts with 1, 2
Console.WriteLine(Describe(arr3)); // One element: 1
Console.WriteLine(Describe(arr4)); // Empty array
Pattern Matching with OOP
abstract class Shape { }
class Circle : Shape { public double Radius; }
class Rectangle : Shape { public double Width, Height; }
class Triangle : Shape { public double Base, Height; }
static double GetArea(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
Triangle t => 0.5 * t.Base * t.Height,
_ => throw new ArgumentException("Unknown shape")
};
Console.WriteLine(GetArea(new Circle { Radius = 5 })); // 78.54
Console.WriteLine(GetArea(new Rectangle { Width = 4, Height = 6 })); // 24
Console.WriteLine(GetArea(new Triangle { Base = 3, Height = 8 })); // 12
Pattern Matching Quick Reference
┌────────────────────────────────────────┬──────────────────────────────────────┐
│ Pattern │ Example │
├────────────────────────────────────────┼──────────────────────────────────────┤
│ Type pattern │ obj is string s │
│ Constant pattern │ x is 42 or x switch { 0 => ...} │
│ Relational pattern │ n switch { >= 90 => "A" } │
│ Logical: and, or, not │ > 0 and <= 100 │
│ Property pattern │ { Status: "Active", Age: > 18 } │
│ Tuple pattern │ (x, y) switch { (0,0) => "origin" } │
│ List pattern │ arr switch { [1,2,..] => ... } │
│ Discard pattern │ _ => "default" │
└────────────────────────────────────────┴──────────────────────────────────────┘
Pattern matching transforms complex chains of if-else and type-checking code into clean, readable expressions. Once you learn it, you will reach for it constantly — especially when working with inheritance hierarchies, union-like data, and multi-condition logic that previously required many nested blocks.
