Why pattern matching matters
Most business logic boils down to one question: what shape is this data, and what should I do about it? For years, C# developers answered that with long if/else chains and type casts. Since C# 7, and much more so in C# 9–11, the language has grown a rich pattern matching system that answers it declaratively.
Here's the kind of code we want to get away from:
static double Area(object shape)
{
if (shape is Circle)
{
var c = (Circle)shape;
return Math.PI * c.Radius * c.Radius;
}
else if (shape is Rectangle)
{
var r = (Rectangle)shape;
return r.Width * r.Height;
}
throw new ArgumentException("Unknown shape");
}It works, but it's noisy: every branch repeats the type check, the cast and the variable.
Switch expressions and type patterns
Combine record types with a switch expression and the same logic collapses into a single expression:
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;
static double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle { Width: var w, Height: var h } => w * h,
Triangle t => 0.5 * t.Base * t.Height,
_ => throw new ArgumentOutOfRangeException(nameof(shape))
};
Each arm is a pattern followed by => and a result. The compiler checks the arms in order and warns you if you've missed a case.
Good code reads like a description of the problem, not a list of instructions for the computer.
Relational and logical patterns
C# 9 added relational operators (<, >= and friends) and the logical keywords and, or and not. Range checks become very readable:
static string Classify(int celsius) => celsius switch
{
< 0 => "Freezing",
>= 0 and < 15 => "Cold",
>= 15 and < 25 => "Pleasant",
_ => "Hot"
};
No more if (x >= 15 && x < 25). The intent is right there on the page.
Property patterns: matching on what's inside
Property patterns let you match on an object's data, including nested properties (extended property patterns arrived in C# 10). Imagine a shipping calculator:
public record Customer(string Country, bool IsPremium);
public record Order(decimal Total, Customer Customer, bool IsExpress);
static decimal ShippingCost(Order order) => order switch
{
{ Customer.IsPremium: true } => 0m,
{ IsExpress: true, Customer.Country: "US" } => 15m,
{ Total: >= 100m } => 0m,
{ Customer.Country: not "US" } => 25m,
_ => 5m
};
Read it out loud and it's the business rule word for word: premium customers ship free; express in the US costs $15; orders over $100 ship free; international costs $25; everything else is $5.
List patterns
C# 11 introduced list patterns, which match arrays and lists by their elements. The slice pattern .. stands for "zero or more elements":
static string Describe(int[] values) => values switch
{
[] => "Empty",
[var only] => $"Just one item: {only}",
[var first, .., var last] => $"Starts with {first}, ends with {last}"
};
This is especially handy for parsing command-line arguments or simple tokens.
Tuple patterns
Need to decide based on two values at once? Switch on a tuple:
static string Winner(string p1, string p2) => (p1, p2) switch
{
var (a, b) when a == b => "Tie",
("rock", "scissors") or ("scissors", "paper") or ("paper", "rock") => "Player 1",
_ => "Player 2"
};
Tips for using patterns well
Order matters. Arms are checked top to bottom, so put the most specific patterns first.
Let the compiler help. Avoid a catch-all
_when you can; without it, the compiler will warn you about unhandled cases.Pair patterns with records. Immutable records and positional deconstruction are a natural fit.
Don't over-nest. If a single pattern takes more than a line or two to read, pull it out into a well-named method.
A few quick rules of thumb:
Use
isfor a single check:if (obj is string { Length: > 0 } s)Use a
switchexpression when you're producing a valueUse a
switchstatement when each branch performs side effects