C# Pattern Matching — Switch Expressions, Property, Positional, and Tuple Patterns
In this tutorial, you will learn about C# Pattern Matching. We cover key concepts, practical examples, and best practices to help you master this topic.
C# pattern matching allows checking the shape of data against patterns including type, property, positional, tuple, relational, and list patterns, enabling concise and expressive conditional code.
What You'll Learn
You will master pattern matching in C#: switch expressions for concise value-based logic, property patterns for matching object properties, positional patterns for deconstructing types, tuple patterns for multi-value matching, list patterns for array/collection matching, and how pattern matching integrates with .NET record types.
Why It Matters
Pattern matching transforms how you write conditional logic in C#. It eliminates long if/else chains and switch statements, making intent explicit and code more readable. Combined with records, pattern matching enables functional-style data processing. The compiler also checks exhaustiveness, ensuring all cases are handled.
Real-World Use
E-commerce systems use pattern matching for pricing rules. Workflow engines use property patterns for state machine transitions. API controllers use tuple patterns for status code determination. Logging systems use type patterns for different log entry types. Game Development uses pattern matching for game state processing.
Learning Path
graph LR
A["26: Nullable Reference Types"] --> B["27: Pattern Matching"]
B --> C["28: Records & Structs"]
C --> D["29: Async Await"]
D --> E["30: Parallel Programming"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Type Pattern
object value = 42;
// Type pattern in if
if (value is int number)
{
Console.WriteLine($"It's an integer: {number}");
}
// Type pattern in switch
string Describe(object obj) => obj switch
{
int i => $"Integer: {i}",
string s => $"String: \"{s}\"",
double d => $"Double: {d:F2}",
null => "Null value",
_ => $"Unknown type: {obj.GetType().Name}"
};
Console.WriteLine(Describe(42)); // Integer: 42
Console.WriteLine(Describe("Hello")); // String: "Hello"
Console.WriteLine(Describe(null)); // Null value
Property Pattern
Match on the properties of an object:
public record Order(decimal Total, string Status, bool IsVip, int ItemCount);
decimal CalculateShipping(Order order) => order switch
{
{ Status: "Cancelled" } => 0m,
{ Total: > 100 } => 0m,
{ IsVip: true } => 0m,
{ Total: > 50, Status: "Pending" } => 5m,
{ Total: > 25 } => 10m,
_ => 15m
};
var orders = new[]
{
new Order(200, "Shipped", false, 5),
new Order(75, "Pending", true, 3),
new Order(30, "Pending", false, 2),
new Order(10, "Pending", false, 1),
};
foreach (var order in orders)
Console.WriteLine($"Total: {order.Total:C}, Shipping: {CalculateShipping(order):C}");
Expected output:
Total: $200.00, Shipping: $0.00
Total: $75.00, Shipping: $0.00
Total: $30.00, Shipping: $10.00
Total: $10.00, Shipping: $15.00
Nested Property Patterns
public record Address(string City, string Country);
public record Customer(string Name, Address? Address);
string GetRegion(Customer customer) => customer switch
{
{ Address: { Country: "USA" } } => "Domestic",
{ Address: { Country: "Canada" } } => "North America",
{ Address: { Country: var c } } => $"International: {c}",
{ Address: null } => "Unknown",
_ => "Other"
};
var customers = new[]
{
new Customer("Alice", new Address("NYC", "USA")),
new Customer("Bob", new Address("Toronto", "Canada")),
new Customer("Charlie", new Address("London", "UK")),
new Customer("Diana", null)
};
foreach (var c in customers)
Console.WriteLine($"{c.Name}: {GetRegion(c)}");
Positional Pattern
Works with types that implement Deconstruct (records, tuples):
public record Point(int X, int Y);
string ClassifyPoint(Point p) => p switch
{
(0, 0) => "Origin",
(0, _) => "On Y-axis",
(_, 0) => "On X-axis",
(> 0, > 0) => "First quadrant",
(< 0, > 0) => "Second quadrant",
(< 0, < 0) => "Third quadrant",
(> 0, < 0) => "Fourth quadrant",
_ => "Unknown"
};
Console.WriteLine(ClassifyPoint(new Point(0, 0))); // Origin
Console.WriteLine(ClassifyPoint(new Point(3, 4))); // First quadrant
Console.WriteLine(ClassifyPoint(new Point(-1, 5))); // Second quadrant
Tuple Pattern
Match on multiple values simultaneously:
string GetGameResult(int playerScore, int opponentScore) => (playerScore, opponentScore) switch
{
(> 100, _) => "Player wins by threshold",
(_, > 100) => "Opponent wins by threshold",
var (p, o) when p > o => $"Player wins {p}-{o}",
var (p, o) when p < o => $"Opponent wins {o}-{p}",
(_, _) => "Tie"
};
Console.WriteLine(GetGameResult(120, 50)); // Player wins by threshold
Console.WriteLine(GetGameResult(80, 60)); // Player wins 80-60
Console.WriteLine(GetGameResult(50, 50)); // Tie
Rock-Paper-Scissors with Tuple Pattern
string RockPaperScissors(string player1, string player2) =>
(player1.ToLower(), player2.ToLower()) switch
{
("rock", "scissors") or ("scissors", "paper") or ("paper", "rock") => "Player 1 wins",
var (p1, p2) when p1 == p2 => "Tie",
_ => "Player 2 wins"
};
Console.WriteLine(RockPaperScissors("rock", "scissors")); // Player 1 wins
Console.WriteLine(RockPaperScissors("paper", "rock")); // Player 1 wins
Console.WriteLine(RockPaperScissors("scissors", "rock")); // Player 2 wins
Relational Pattern
Use relational operators in patterns:
string ClassifyTemperature(double celsius) => celsius switch
{
< -40 => "Extreme cold",
< 0 => "Freezing",
< 15 => "Cold",
< 25 => "Mild",
< 35 => "Warm",
< 45 => "Hot",
_ => "Extreme heat"
};
// Combined relational patterns
string ClassifyAge(int age) => age switch
{
< 0 or > 150 => "Invalid age",
>= 0 and < 13 => "Child",
>= 13 and < 20 => "Teenager",
>= 20 and < 65 => "Adult",
_ => "Senior"
};
List Pattern (C# 11)
Match on arrays and collections:
int[] numbers = { 1, 2, 3, 4, 5 };
string DescribeArray(int[] values) => values switch
{
[] => "Empty",
[var first] => $"Single element: {first}",
[var first, var second] => $"Two elements: {first}, {second}",
[.., var last] => $"Multiple elements, last: {last}",
[.., var secondLast, _] => $"At least two elements, second last: {secondLast}",
_ => "Unknown"
};
Console.WriteLine(DescribeArray(Array.Empty<int>())); // Empty
Console.WriteLine(DescribeArray(new[] { 42 })); // Single element: 42
Console.WriteLine(DescribeArray(new[] { 1, 2 })); // Two elements: 1, 2
Console.WriteLine(DescribeArray(new[] { 1, 2, 3 })); // Multiple elements, last: 3
// List pattern with slice
int[] data = { 0, 1, 2, 3, 4, 5 };
if (data is [0, .. var middle, 5])
{
Console.WriteLine($"Middle elements: {string.Join(", ", middle)}"); // 1, 2, 3, 4
}
Constant Pattern
string GetDayType(DayOfWeek day) => day switch
{
DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
DayOfWeek.Monday => "Start of week",
DayOfWeek.Friday => "End of week",
_ => "Workday"
};
Var Pattern
// Var pattern captures the value for further processing
string ProcessScore(int score) => score switch
{
< 0 => "Invalid",
var s when s >= 90 => "A",
var s when s >= 80 => "B",
var s when s >= 70 => "C",
var s when s >= 60 => "D",
var s => "F"
};
Exhaustiveness
The compiler checks that all cases are covered:
enum Color { Red, Green, Blue }
string GetHex(Color color) => color switch
{
Color.Red => "#FF0000",
Color.Green => "#00FF00",
Color.Blue => "#0000FF",
_ => "#000000" // Without this, compiler warns about incompleteness
};
Common Mistakes
Mistake 1: Not Covering All Cases
Switch expressions must be exhaustive. Missing cases cause compilation warnings (or errors with strict settings). Always include the discard pattern _.
Mistake 2: Confusing Property Pattern with Positional Pattern
Property patterns use { Property: value }. Positional patterns use (value1, value2). They look similar but work differently. Property patterns match named properties; positional patterns use Deconstruct.
Mistake 3: Overcomplicating Patterns
Nested patterns can become hard to read. Extract complex pattern matching into separate methods with descriptive names.
Mistake 4: Using when Clauses Incorrectly
when clauses must come after the pattern: case int i when i > 0:. The pattern must match before the condition is evaluated.
Mistake 5: Forgetting Parentheses in Relational Patterns
> 0 and < 10 requires parentheses sometimes: (> 0) and (< 10). The compiler handles most cases, but add parentheses for clarity.
Mistake 6: Pattern Matching on Non-Exhaustive Types
Enums and nullable value types should include null cases. The compiler warns if your switch expression does not cover all possible values.
Practice Questions
- What is the difference between a type pattern and a property pattern?
- How do tuple patterns differ from positional patterns?
- Why is the discard pattern
_important in switch expressions? - Write a pattern-matching expression that categorizes a person by age range (infant, child, teen, adult, senior).
- How does the list pattern work? Give an example with slice patterns.
Challenge
Create a shape area calculator using pattern matching. Define Shape as a discriminated union using records: Circle(double Radius), Rectangle(double Width, double Height), Triangle(double Base, double Height). Use a switch expression to calculate area for each shape.
FAQ
Mini Project
Create a tax calculator using pattern matching:
public record TaxPayer(string Name, decimal Income, bool IsSelfEmployed, bool HasDependents, string State);
class TaxCalculator
{
public decimal CalculateTax(TaxPayer payer) => payer switch
{
{ Income: <= 0 } => 0m,
{ Income: <= 11000 } => IncomeTax(payer.Income, 0.10m),
{ Income: <= 44725 } => IncomeTax(payer.Income, 0.12m),
{ Income: <= 95375 } => IncomeTax(payer.Income, 0.22m),
{ Income: <= 182100 } => IncomeTax(payer.Income, 0.24m),
{ Income: <= 231250 } => IncomeTax(payer.Income, 0.32m),
{ Income: <= 578125 } => IncomeTax(payer.Income, 0.35m),
_ => IncomeTax(payer.Income, 0.37m)
};
public decimal CalculateDeductions(TaxPayer payer) => payer switch
{
{ HasDependents: true, IsSelfEmployed: true } => 28000m,
{ HasDependents: true } => 20000m,
{ IsSelfEmployed: true } => 15000m,
_ => 13850m // Standard deduction
};
public decimal CalculateNetTax(TaxPayer payer)
{
var taxableIncome = Math.Max(0, payer.Income - CalculateDeductions(payer));
var federalTax = taxableIncome switch
{
<= 0 => 0m,
<= 11000 => taxableIncome * 0.10m,
<= 44725 => 1100m + (taxableIncome - 11000) * 0.12m,
<= 95375 => 5147m + (taxableIncome - 44725) * 0.22m,
<= 182100 => 16290m + (taxableIncome - 95375) * 0.24m,
<= 231250 => 37104m + (taxableIncome - 182100) * 0.32m,
<= 578125 => 52832m + (taxableIncome - 231250) * 0.35m,
_ => 174238m + (taxableIncome - 578125) * 0.37m
};
return federalTax;
}
private static decimal IncomeTax(decimal income, decimal rate) => income * rate;
public string GetTaxSummary(TaxPayer payer) => (payer, CalculateNetTax(payer)) switch
{
(_, 0) => $"{payer.Name}: No tax due",
var (p, tax) when tax < 1000 => $"{p.Name}: Low tax bracket (${tax:N0})",
var (p, tax) when tax < 10000 => $"{p.Name}: Moderate tax (${tax:N0})",
var (p, tax) => $"{p.Name}: High earner, tax: ${tax:N0}"
};
}
var calculator = new TaxCalculator();
var taxpayers = new[]
{
new TaxPayer("Alice", 55000, false, true, "CA"),
new TaxPayer("Bob", 120000, true, false, "TX"),
new TaxPayer("Charlie", 25000, false, false, "NY"),
new TaxPayer("Diana", 600000, false, true, "FL"),
};
foreach (var p in taxpayers)
{
var deduction = calculator.CalculateDeductions(p);
var netTax = calculator.CalculateNetTax(p);
Console.WriteLine(calculator.GetTaxSummary(p));
Console.WriteLine($" Income: {p.Income:C}, Deduction: {deduction:C}, Tax: {netTax:C}\n");
}
Expected output:
Alice: Moderate tax ($5,569)
Income: $55,000.00, Deduction: $20,000.00, Tax: $5,569.00
Bob: High earner, tax: $18,389
Income: $120,000.00, Deduction: $15,000.00, Tax: $18,389.00
Charlie: Low tax bracket ($1,115)
Income: $25,000.00, Deduction: $13,850.00, Tax: $1,115.00
Diana: High earner, tax: $181,548
Income: $600,000.00, Deduction: $20,000.00, Tax: $181,548.00
What's Next
You have mastered pattern matching in C#. The next lesson covers async and await: Task
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro