Pattern Matching & Switch Expressions
Pattern matching lets you test a value against a shape and extract information from it in a single, concise expression. C# has grown a rich set of patterns since C# 7, with the most powerful additions arriving in C# 8–11.
is Pattern
The simplest form checks type and binds a variable in one step:
if (obj is string s)
Console.WriteLine(s.Length);Switch Expression (C# 8+)
The switch expression (as opposed to the switch statement) evaluates to a value and is exhaustive — the compiler warns if cases are not covered. Arms use => instead of case/break:
string label = shape switch
{
Circle c => ````````circle r=${c.Radius}````````,
Rectangle r => ````````rect ${r.W}×${r.H}````````,
_ => "unknown"
};Property Patterns
Match on the values of properties without needing a variable:
if (order is { Status: OrderStatus.Shipped, Total: > 1000 })
ApplyDiscount(order);Positional Patterns
Work with types that have a Deconstruct method (including tuples and records):
var point = new Point(1, -1);
if (point is (> 0, < 0))
Console.WriteLine("Quadrant IV");List Patterns (C# 11+)
Match arrays or lists by their element structure:
int[] arr = { 1, 2, 3 };
if (arr is [1, .., 3]) // starts with 1, ends with 3
Console.WriteLine("Matched");when Guards
Add extra conditions to any pattern arm:
shape switch
{
Circle c when c.Radius > 100 => "huge circle",
Circle c => "small circle",
}Type Patterns and Recursive Patterns
You can nest patterns: a type pattern inside a property pattern, or a property pattern inside a positional pattern. This allows deeply structured matching in a single expression, replacing chains of if/else if with is checks.
Code Examples
using System;
abstract record Shape;
record Circle(double Radius) : Shape;
record Rectangle(double Width, double Height) : Shape;
record Triangle(double Base, double Height) : Shape;
static double Area(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,
null => throw new ArgumentNullException(nameof(shape)),
_ => throw new NotSupportedException(````````Unknown shape: ${shape.GetType().Name}````````),
};
static string Classify(Shape shape) => shape switch
{
Circle c when c.Radius > 10 => "Large circle",
Circle c when c.Radius > 5 => "Medium circle",
Circle => "Small circle",
Rectangle r when r.Width == r.Height => "Square",
Rectangle => "Rectangle",
_ => "Other shape",
};
Shape[] shapes = { new Circle(3), new Circle(7), new Circle(15),
new Rectangle(4, 4), new Rectangle(5, 3), new Triangle(6, 4) };
foreach (var s in shapes)
{
Console.WriteLine(````````${Classify(s),-18} area = ${Area(s):F2}````````);
}Switch expressions evaluate top-to-bottom and return a value. 'when' guards refine type matches. The discard '_' arm acts as the default. The compiler verifies exhaustiveness.
using System;
public record Order(int Id, string Status, decimal Total, string Country);
static string ClassifyOrder(Order order) => order switch
{
// Property pattern — match on property values
{ Status: "Cancelled" } => "Cancelled order",
{ Status: "Shipped", Total: > 500, Country: "US" } => "High-value US shipment",
{ Status: "Shipped", Total: > 500 } => "High-value international shipment",
{ Status: "Shipped" } => "Standard shipment",
{ Status: "Pending", Total: 0 } => "Empty pending order",
{ Status: "Pending" } => "Pending order",
_ => "Unknown status",
};
var orders = new[]
{
new Order(1, "Pending", 0m, "UK"),
new Order(2, "Pending", 120m, "UK"),
new Order(3, "Shipped", 650m, "US"),
new Order(4, "Shipped", 650m, "DE"),
new Order(5, "Shipped", 80m, "FR"),
new Order(6, "Cancelled", 200m, "US"),
};
foreach (var o in orders)
Console.WriteLine(````````Order ${o.Id}: ${ClassifyOrder(o)}````````);Property patterns match multiple property values simultaneously using { Property: pattern } syntax. More specific arms should come before general ones, just like catch blocks.
using System;
record Address(string City, string Country);
record Customer(string Name, Address Address, decimal CreditLimit);
static string RouteCustomer(Customer customer) => customer switch
{
// Nested property pattern — drill into Address from within Customer pattern
{ Address: { Country: "US", City: "New York" }, CreditLimit: > 10_000 }
=> "Premium NYC client → dedicated manager",
{ Address: { Country: "US" }, CreditLimit: > 5_000 }
=> "High-value US client → express support",
{ Address: { Country: "US" } }
=> "Standard US client → self-service",
{ Address: { Country: var c }, CreditLimit: > 5_000 }
=> ````````High-value client in ${c} → international team````````,
{ Address: { Country: var c } }
=> ````````Standard client in ${c} → regional support````````,
};
// List pattern (C# 11+)
static string DescribeList(int[] data) => data switch
{
[] => "Empty list",
[var single] => ````````Single element: ${single}````````,
[var first, .., var last] => ````````${data.Length} elements, first=${first}, last=${last}````````,
};
var customers = new[]
{
new Customer("Alice", new Address("New York", "US"), 15_000m),
new Customer("Bob", new Address("Chicago", "US"), 6_000m),
new Customer("Carol", new Address("London", "UK"), 8_000m),
new Customer("Dave", new Address("Berlin", "DE"), 2_000m),
};
foreach (var c in customers)
Console.WriteLine(````````${c.Name}: ${RouteCustomer(c)}````````);
Console.WriteLine();
Console.WriteLine(DescribeList(Array.Empty<int>()));
Console.WriteLine(DescribeList(new[] { 42 }));
Console.WriteLine(DescribeList(new[] { 1, 2, 3, 4, 5 }));Nested property patterns allow matching deep object graphs in one expression. The 'var' pattern binds a value inside a pattern for use in the arm. List patterns (C# 11) match arrays by position and can use '..' to skip middle elements.
Quick Quiz
1. What is the key syntactic difference between a switch statement and a switch expression in C#?
2. What does a property pattern like '{ Status: "Shipped", Total: > 100 }' check?
3. What does the list pattern '[var first, .., var last]' match?
Was this lesson helpful?