Pattern matching in C# — is, switch, property, and list patterns
Pattern matching lets you inspect and destructure values in a single expression. It started in C# 7 with is type patterns and has expanded across every subsequent version — C# 8 added property patterns, C# 9 added relational and logical patterns, C# 10 added extended property patterns, and C# 11 added list patterns.
All patterns work in both is expressions and switch expressions.
Type Pattern — is T variable
Test the type of an object and bind it to a new variable in one step. No cast required.
object shape = new Circle(5.0);
// Old way (C# 5 and earlier)
if (shape is Circle)
{
Circle c = (Circle)shape; // separate cast
Console.WriteLine(c.Radius);
}
// Type pattern (C# 7+) — test and bind in one expression
if (shape is Circle circle)
Console.WriteLine($"Circle with radius {circle.Radius}"); // Circle with radius 5
// Works with interfaces
object value = "hello";
if (value is IEnumerable<char> chars)
Console.WriteLine(chars.Count()); // 5
record Circle(double Radius);Constant Pattern
Match a specific value — useful for null checks and literal comparisons.
object? obj = null;
// Null check with constant pattern
if (obj is null)
Console.WriteLine("obj is null"); // obj is null
if (obj is not null)
Console.WriteLine("has value");
// Constant values
int code = 404;
string message = code switch
{
200 => "OK",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Unknown"
};
Console.WriteLine(message); // Not FoundRelational Pattern (C# 9+)
Compare values without writing the variable name twice.
int score = 78;
string grade = score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
Console.WriteLine(grade); // C
// Works with is too
double temperature = -5.0;
if (temperature is < 0)
Console.WriteLine("Freezing"); // Freezing
if (temperature is >= 0 and < 25)
Console.WriteLine("Comfortable");Logical Patterns — and, or, not (C# 9+)
Combine patterns with and, or, and not keywords.
// not — negate a pattern
object? val = "hello";
if (val is not null)
Console.WriteLine("not null"); // not null
// and — both patterns must match
int age = 25;
if (age is >= 18 and <= 65)
Console.WriteLine("Working age"); // Working age
// or — either pattern matches
char ch = 'e';
if (ch is 'a' or 'e' or 'i' or 'o' or 'u')
Console.WriteLine("Vowel"); // Vowel
// Combine in a switch expression
double speed = 95.0;
string zone = speed switch
{
< 0 => "Invalid",
0 => "Stopped",
> 0 and < 30 => "Slow",
>= 30 and < 70 => "Normal",
>= 70 and < 120 => "Fast",
_ => "Dangerous"
};
Console.WriteLine(zone); // FastProperty Pattern
Inspect properties without extracting them to variables first.
public record Order(string Status, decimal Total, string Country);
var order = new Order("Shipped", 250.00m, "US");
// Match on one property
if (order is { Status: "Shipped" })
Console.WriteLine("Your order is on its way!");
// Match on multiple properties
string message = order switch
{
{ Status: "Cancelled" } => "Order cancelled.",
{ Status: "Shipped", Country: "US" } => "US shipment in progress.",
{ Status: "Shipped", Total: >= 200, Country: not "US" } => "International express.",
{ Status: "Pending" } => "Awaiting processing.",
_ => "Check order status."
};
Console.WriteLine(message); // US shipment in progress.Extended Property Pattern (C# 10+)
Navigate nested properties using . inside the pattern.
public record Address(string City, string Country);
public record Customer(string Name, Address Address);
var customer = new Customer("Alice", new Address("London", "UK"));
// C# 9: nested property pattern
if (customer is { Address: { Country: "UK" } })
Console.WriteLine("UK customer");
// C# 10: extended — same but shorter
if (customer is { Address.Country: "UK" })
Console.WriteLine("UK customer"); // UK customerPositional Pattern
Deconstructs an object and matches on the deconstructed values. Works with records and types that have a Deconstruct method.
public record Point(int X, int Y);
var point = new Point(3, -1);
string quadrant = point switch
{
( > 0, > 0) => "Q1 (+,+)",
( < 0, > 0) => "Q2 (-,+)",
( < 0, < 0) => "Q3 (-,-)",
( > 0, < 0) => "Q4 (+,-)",
(0, 0) => "Origin",
_ => "On an axis"
};
Console.WriteLine(quadrant); // Q4 (+,-)List Pattern (C# 11+)
Match the structure and contents of a list or array.
int[] numbers = { 1, 2, 3, 4, 5 };
// Starts with 1 and 2
if (numbers is [1, 2, ..])
Console.WriteLine("Starts with 1, 2"); // Starts with 1, 2
// Exactly three elements, middle is anything
if (numbers is [_, _, _, _, _])
Console.WriteLine("Has exactly 5 elements"); // Has exactly 5 elements
// Ends with 4 and 5
if (numbers is [.., 4, 5])
Console.WriteLine("Ends with 4, 5"); // Ends with 4, 5
// Practical: parse a command split on spaces
string[] args = { "move", "left", "10" };
string result = args switch
{
["move", var direction, var amount] => $"Moving {direction} by {amount}",
["stop"] => "Stopping",
[] => "No command",
_ => "Unknown command"
};
Console.WriteLine(result); // Moving left by 10Real-World Example: Discount Calculator
public record Cart(string CustomerType, decimal Total, bool HasCoupon);
static decimal GetDiscount(Cart cart) => cart switch
{
{ CustomerType: "VIP", Total: >= 500 } => 0.20m, // 20% — VIP high spender
{ CustomerType: "VIP" } => 0.15m, // 15% — VIP standard
{ CustomerType: "Member", HasCoupon: true } => 0.12m, // 12% — member + coupon
{ CustomerType: "Member" } => 0.08m, // 8% — member
{ HasCoupon: true, Total: >= 100 } => 0.05m, // 5% — guest + coupon + min spend
_ => 0.00m // no discount
};
var cart1 = new Cart("VIP", 750m, false);
var cart2 = new Cart("Member", 200m, true);
var cart3 = new Cart("Guest", 150m, true);
Console.WriteLine($"VIP: {GetDiscount(cart1):P0}"); // 20%
Console.WriteLine($"Member: {GetDiscount(cart2):P0}"); // 12%
Console.WriteLine($"Guest: {GetDiscount(cart3):P0}"); // 5%