Pattern Matching in C#
is expressions, switch expressions, property patterns, positional patterns, and list patterns in C#.
Type Patterns with is
The is operator tests a value’s type and optionally binds it to a new variable:
object value = "Hello, C#!";
// Old style — cast then use
if (value is string)
{
string s = (string)value;
Console.WriteLine(s.ToUpper());
}
// Type pattern — combines check and cast
if (value is string s)
Console.WriteLine(s.ToUpper()); // s is string, available in this scope
// Works for null checks
if (value is not null)
Console.WriteLine("Not null");
// Nested with 'and'
if (value is string { Length: > 5 } longString)
Console.WriteLine($"Long string: {longString}");
Constant Patterns
int statusCode = 404;
string message = statusCode switch
{
200 => "OK",
201 => "Created",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
500 => "Internal Server Error",
_ => $"Unknown status: {statusCode}"
};
// With strings
string lang = "fr";
string greeting = lang switch
{
"en" => "Hello",
"fr" => "Bonjour",
"de" => "Hallo",
"es" => "Hola",
_ => "Hi"
};
Relational Patterns
double bmi = 22.5;
string category = bmi switch
{
< 18.5 => "Underweight",
>= 18.5 and < 25 => "Normal",
>= 25 and < 30 => "Overweight",
>= 30 => "Obese",
double.NaN => "Invalid",
_ => "Unknown"
};
// Combined with type pattern
object obj = 42;
bool isPositiveInt = obj is int n and > 0;
Property Patterns
Match against an object’s properties without first assigning it to a variable:
record Address(string Street, string City, string Country, string PostalCode);
record Customer(string Name, Address Address, bool IsPremium, decimal CreditLimit);
Customer customer = new("Alice", new("123 Main St", "London", "UK", "SW1A 1AA"), true, 5000m);
// Property pattern
bool isEligible = customer is
{
IsPremium: true,
CreditLimit: >= 1000,
Address.Country: "UK"
};
// Switch with property patterns
decimal discount = customer switch
{
{ IsPremium: true, CreditLimit: >= 5000 } => 0.20m,
{ IsPremium: true } => 0.10m,
{ Address.Country: "UK" } => 0.05m,
_ => 0m
};
// Nested property patterns
string zone = customer switch
{
{ Address: { Country: "US", PostalCode: var zip } } when zip.StartsWith("9")
=> "US West",
{ Address.Country: "US" } => "US East",
{ Address.Country: "UK" } => "Europe",
_ => "Other"
};
Positional Patterns
Positional patterns match against a type’s Deconstruct method. Records get one automatically.
record Point(int X, int Y);
record Size(int Width, int Height);
var point = new Point(3, -1);
string quadrant = point switch
{
(0, 0) => "Origin",
(> 0, > 0) => "Q1",
(< 0, > 0) => "Q2",
(< 0, < 0) => "Q3",
(> 0, < 0) => "Q4",
(_, 0) => "X-axis",
(0, _) => "Y-axis",
_ => "Unknown"
};
Console.WriteLine(quadrant); // Q4
// Deconstruct a tuple
(string role, int level) user = ("admin", 5);
string permission = user switch
{
("admin", >= 5) => "Full access",
("admin", _) => "Limited admin",
("editor", _) => "Edit access",
_ => "Read only"
};
// Custom Deconstruct
public class Rectangle
{
public int Width { get; init; }
public int Height { get; init; }
public void Deconstruct(out int width, out int height) => (width, height) = (Width, Height);
}
var rect = new Rectangle { Width = 10, Height = 5 };
string shape = rect switch
{
(var w, var h) when w == h => "Square",
(> 0, > 0) => "Valid rectangle",
_ => "Invalid"
};
List Patterns (C# 11)
List patterns match against the structure of a sequence:
int[] empty = { };
int[] one = { 1 };
int[] two = { 1, 2 };
int[] many = { 1, 2, 3, 4, 5 };
string Describe(int[] arr) => arr switch
{
[] => "empty",
[var x] => $"single: {x}",
[var x, var y] => $"pair: {x}, {y}",
[1, 2, ..] => "starts with 1, 2",
[.., 5] => "ends with 5",
_ => $"{arr.Length} elements"
};
Console.WriteLine(Describe(empty)); // empty
Console.WriteLine(Describe(one)); // single: 1
Console.WriteLine(Describe(two)); // pair: 1, 2
Console.WriteLine(Describe(many)); // starts with 1, 2
// Capture the rest of the sequence
if (many is [var first, .. var rest])
Console.WriteLine($"First: {first}, Rest: [{string.Join(", ", rest)}]");
// First: 1, Rest: [2, 3, 4, 5]
// Parse command-line args with list patterns
string[] args = { "run", "--port", "8080", "--env", "production" };
string result = args switch
{
["run", "--port", var port, ..] => $"Running on port {port}",
["build", ..] => "Building",
[] => "No arguments",
_ => "Unknown command"
};
var Pattern
The var pattern always matches and captures the value — useful in switch guards:
string? input = GetInput();
bool valid = input switch
{
null => false,
var s when s.Length > 100 => false, // too long
var s when s.All(char.IsDigit) => true,
_ => false
};
Combining Patterns
// 'and' — both must match
bool isShortPositive = value is int n and > 0 and < 100;
// 'or' — either must match
bool isEdgeValue = value is 0 or int.MaxValue;
// 'not' — negation
bool isNotNull = value is not null;
bool isNotString = value is not string;
// Complex combination
object result = GetResult();
string summary = result switch
{
null => "null result",
string s and { Length: 0 } => "empty string",
string s => $"string: {s}",
int n and (< 0 or > 1000) => $"out of range int: {n}",
int n => $"int: {n}",
IEnumerable<int> { } list => $"list with {list.Count()} items",
_ => $"unknown: {result.GetType().Name}"
};
Real-World Example: HTTP Response Handling
record ApiResponse(int StatusCode, string? Body, string? ErrorMessage);
string HandleResponse(ApiResponse response) => response switch
{
{ StatusCode: 200, Body: var body } when body is not null
=> $"Success: {body}",
{ StatusCode: 201, Body: var location }
=> $"Created at: {location ?? "unknown"}",
{ StatusCode: >= 400 and < 500, ErrorMessage: var msg }
=> $"Client error {response.StatusCode}: {msg ?? "no detail"}",
{ StatusCode: >= 500, ErrorMessage: var msg }
=> $"Server error {response.StatusCode}: {msg ?? "internal error"}",
_ => $"Unexpected status: {response.StatusCode}"
}; Frequently Asked Questions
What is the difference between is and as?
'is' returns true/false and can bind a variable. 'as' returns the cast result or null without throwing. Use 'is' with a variable binding (if obj is string s) over 'as' plus null check — it's cleaner and works for non-nullable value types too.
What are positional patterns?
Positional patterns match against a type's Deconstruct method. If a type has Deconstruct(out int x, out int y), you can match it with (> 0, > 0) and the compiler calls Deconstruct for you. Records get Deconstruct generated automatically.
What are list patterns?
List patterns (C# 11) match against sequences. [1, 2, ..] matches a sequence starting with 1, 2. [_, _, ..rest] matches any sequence with at least 2 elements and captures the remainder. They work on arrays, spans, and any type implementing a Length/Count and indexer.