Pattern matching has evolved significantly since its introduction in C# 7. What began as a convenient way to check object types has grown into a powerful language feature that simplifies conditional logic, improves readability, and reduces boilerplate code.
Modern C# versions introduce property patterns, relational patterns, logical patterns, list patterns, and enhanced switch expressions, enabling developers to express complex conditions in a concise and maintainable way.
In this article, we'll explore advanced pattern matching techniques with practical examples and discuss when they improve code quality—and when they may reduce readability.
Why Use Pattern Matching?
Traditional conditional logic often involves multiple if statements, type checks, and explicit casts.
For example:
if (shape is Circle)
{
var circle = (Circle)shape;
Console.WriteLine(circle.Radius);
}
Pattern matching combines the type check and variable declaration into a single expression.
if (shape is Circle circle)
{
Console.WriteLine(circle.Radius);
}
The result is cleaner, safer, and easier to read.
Type Patterns
Type patterns allow you to verify an object's type while creating a strongly typed variable.
public static double CalculateArea(object shape)
{
return shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
Triangle t => t.Base * t.Height / 2,
_ => throw new ArgumentException("Unknown shape")
};
}
This eliminates explicit casts and keeps the implementation concise.
Property Patterns
Property patterns evaluate object properties directly without nested conditionals.
Instead of:
if (customer != null &&
customer.IsActive &&
customer.Orders > 10)
{
Console.WriteLine("Premium customer");
}
Use:
if (customer is
{
IsActive: true,
Orders: > 10
})
{
Console.WriteLine("Premium customer");
}
Property patterns are particularly useful when validating DTOs, API requests, or domain objects.
Relational Patterns
Relational patterns simplify numeric comparisons.
string category = price switch
{
< 100 => "Budget",
>= 100 and < 500 => "Standard",
>= 500 and < 1000 => "Premium",
_ => "Luxury"
};
Compared to multiple if statements, the intent is much clearer.
Logical Patterns
Logical patterns combine conditions using and, or, and not.
Example:
if (age is >= 18 and < 65)
{
Console.WriteLine("Working age");
}
Using or:
if (status is "Pending" or "Processing")
{
Console.WriteLine("Order is active.");
}
Using not:
if (customer is not null)
{
Console.WriteLine(customer.Name);
}
These operators improve readability by replacing deeply nested conditions.
Switch Expressions
Modern switch expressions are concise and expressive.
Traditional switch:
switch (day)
{
case DayOfWeek.Saturday:
case DayOfWeek.Sunday:
return false;
default:
return true;
}
Modern expression:
return day switch
{
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false,
_ => true
};
Switch expressions work particularly well for mapping values and implementing business rules.
List Patterns
List patterns, introduced in newer versions of C#, allow matching arrays and collections by structure.
Example:
if (numbers is [1, 2, 3])
{
Console.WriteLine("Exact sequence.");
}
Matching a prefix:
if (numbers is [0, ..])
{
Console.WriteLine("Starts with zero.");
}
Matching a suffix:
if (numbers is [.., 100])
{
Console.WriteLine("Ends with 100.");
}
List patterns are useful for parsing structured input and validating sequences.
Nested Patterns
Pattern matching also supports nested object structures.
if (order is
{
Customer:
{
IsActive: true
},
Total: > 1000
})
{
Console.WriteLine("Priority order");
}
Complex object validation can often be expressed in a single readable statement.
Pattern Matching with Null Checks
Null checking becomes simpler using modern C# syntax.
Instead of:
if (customer != null)
{
Process(customer);
}
Use:
if (customer is not null)
{
Process(customer);
}
While functionally equivalent, the pattern matching form integrates naturally with other pattern expressions.
Best Practices
Use switch expressions for value mapping instead of lengthy switch statements.
Prefer property patterns over deeply nested if conditions.
Combine relational and logical patterns for readable range checks.
Use list patterns only when they improve clarity.
Keep pattern matching expressions focused and easy to understand.
Choose descriptive variable names in type patterns.
Refactor overly complex pattern expressions into smaller methods when necessary.
Common Mistakes
Overusing Nested Patterns
Pattern matching can express complex conditions, but deeply nested expressions quickly become difficult to read. If a pattern spans several lines and obscures business intent, consider extracting the logic into a well-named helper method.
Replacing Every if Statement
Not every conditional benefits from pattern matching. Simple conditions are often clearer with a straightforward if statement.
Ignoring Readability
Pattern matching should simplify code, not make it more cryptic. Favor readability over reducing the number of lines.
Forgetting Exhaustive Cases
When using switch expressions, always include a default (_) case or handle every possible input. This prevents unexpected runtime exceptions when new values are introduced.
Pattern Matching Features at a Glance
| Feature | Best Use Case |
|---|
| Type patterns | Safe type checking and casting |
| Property patterns | Object validation |
| Relational patterns | Numeric comparisons |
| Logical patterns | Combining conditions |
| Switch expressions | Mapping values to results |
| List patterns | Matching array and collection structures |
| Nested patterns | Validating complex object graphs |
Conclusion
Pattern matching has become one of the most expressive features in modern C#, allowing developers to write cleaner, safer, and more maintainable code. Features such as type patterns, property patterns, relational patterns, logical operators, list patterns, and switch expressions reduce boilerplate while making business logic easier to understand.
However, as with any language feature, moderation is important. Pattern matching should simplify your code—not make it harder to follow. When used thoughtfully, it can replace verbose conditionals with expressive, declarative logic that improves both readability and maintainability.
By understanding the strengths of each pattern matching feature and applying them where they provide genuine clarity, you can take full advantage of modern C# and write code that is easier to maintain as your applications grow.