Back to Blog
C#

C# Is Pattern Matching: Syntax and Use

c# is pattern matching: Learn how the `is` pattern in C# works, from type checks to property and relational patterns, with practical code examples and performance insi...

pattern matchingC# syntaxswitch expressionsproperty patternsrelational patterns
C# code with is pattern matching syntax, featuring a highlighted is expression and property pattern with visual cues of type checking

The is keyword in C# has evolved from a simple type-checking operator into a full pattern-matching tool. When a developer searches for c# is pattern matching, they usually want to understand how is can match not just types, but shapes and values. Since C# 7.0, is supports type patterns, property patterns, relational patterns, and more. This article focuses on the practical syntax and use of is patterns in everyday code.

The Core Syntax of is Patterns

At its core, is tests whether an object matches a pattern. The simplest form is a type pattern:

object value = "hello"; if (value is string text) { Console.WriteLine(text.ToUpper()); }

Here, value is string text does two things: it checks that value is assignable to string, and if so, assigns value to the variable text. This eliminates the need for a separate cast and a null check. The pattern is false if value is null or if the type does not match.

The variable declared in the pattern is scoped to the enclosing block. If you need the variable outside the if, you can declare it before the check:

if (value is string text) { // text is in scope here } // text is not in scope here

For nullable types, a type pattern does not match null. If you want to match null explicitly, use the null pattern: value is null.

Property Patterns: Matching Object Shape

Property patterns extend is to check not just the type, but also the values of properties. This is useful when you need to validate a DTO or a configuration object.

Consider a request object:

public class OrderRequest { public int Quantity { get; set; } public string? CustomerType { get; set; } }

You can match on properties:

if (request is OrderRequest { Quantity: > 0, CustomerType: "VIP" }) { // Apply VIP discount }

Here, the pattern requires request to be non-null, of type OrderRequest, and its Quantity to be greater than zero and its CustomerType to equal "VIP". The > 0 part is a relational pattern. Relational patterns use <, >, <=, and >= to match numeric ranges.

Property patterns are shallow: they only match the immediate properties, not nested properties unless you nest the pattern. For nested properties, you can extend the pattern recursively:

if (order is Order { Customer: { Name: "Alice" } }) { // Order belongs to Alice }

This is equivalent to order?.Customer?.Name == "Alice", but with a more declarative syntax.

Combining Patterns with and and or

C# 9.0 introduced logical patterns: and, or, and not. These combine simpler patterns.

if (value is int i && i > 0 && i < 100) { // valid positive integer under 100 }

The same logic can be expressed with a relational pattern and and:

if (value is int i and > 0 and < 100) { // same condition }

The or pattern matches if either side matches:

if (shape is Circle or Square) { // shape is a circle or square }

And not inverts a pattern. A common use is to check for null without using the null-conditional operator:

if (value is not null) { // value is non-null }

These combinators make complex conditionals easier to read, especially when you chain multiple conditions.

Switch Expressions: Pattern Matching in a Return Value

Pattern matching also appears in switch expressions, which are similar to a switch statement but return a value. They use the same patterns but in a concise form.

public static string Describe(int number) => number switch { 0 => "zero", > 0 => "positive", < 0 => "negative", _ => throw new InvalidOperationException() };

Every arm has a pattern and a value. The _ is the discard pattern, which matches anything. The compiler enforces that the patterns are exhaustive: if no arm matches and there is no discard, the expression throws a SwitchExpressionException.

You can also use property patterns inside switch expressions for more complex dispatch:

public static decimal CalculateDiscount(Order order) => order switch { Order { CustomerType: "VIP", Quantity: > 10 } => 0.2m, Order { CustomerType: "VIP" } => 0.15m, Order { Quantity: > 5 } => 0.1m, _ => 0m };

This is more readable than a chain of if-else statements, especially when the logic is based on multiple conditions.

Runtime Cost and JIT Optimization

Pattern matching has a bad reputation in some circles for being slow, but in modern .NET it's often compiled to efficient checks. The JIT can sometimes optimize simple type patterns to a single type check and cast. Property patterns, however, require reading properties and comparing values, which is generally as fast as doing it manually. A complex pattern such as Order { Customer: { Name: "Alice" } } involves multiple null checks and property accesses, but the compiler may reorder them for efficiency.

The main cost is not the pattern itself but the work done inside the matched branch. If you have nested property patterns, each property access is a virtual or direct call, depending on whether the property is on a class or struct. For structs, property access is direct, but for classes it could trigger virtual dispatch. In hot loops, you might want to avoid deep property patterns, but for most application code the difference is negligible.

There is one important performance consideration: pattern matching is not a way to avoid boxing or unboxing. If you have an object that holds an int, the is int pattern will still unbox the value. That is required because the value is stored as an object. If you care about avoiding boxing, you need to avoid putting value types into object in the first place.

Common Mistakes and Pitfalls

One common mistake is using is with a nullable value type incorrectly. int? is a Nullable<int>, not an int. The pattern x is int will not match a nullable int that has a value? Actually, it will match if the nullable has a value, because the runtime unwraps it. Let's verify: int? y = 5; y is int evaluates to true. The pattern y is int checks if y has a value and that the underlying type is int. So it's equivalent to y.HasValue. On the other hand, y is null checks if y is null (i.e., HasValue is false). This behavior is consistent with how Nullable<T> boxes.

Another mistake is assuming that a property pattern performs a deep equality. Order { Customer: { Name: "Alice" } } does not check Customer for equality; it checks the Name property. It also throws a NullReferenceException if Customer is null, because the pattern tries to read Name. Actually, the pattern will not throw; it will return false. For example, Order order = new Order { Customer = null }; order is Order { Customer: { Name: "Alice" } } returns false without throwing. The pattern evaluator checks if Customer is non-null and then reads Name; it does not throw on null. That is a safe behavior.

A third mistake is forgetting the _ discard in switch expressions. If no arm matches and there is no discard, the expression throws at runtime. Always include a default arm, even if it throws an exception, to make the behavior explicit.

Compatibility and Language Versions

The is pattern syntax is not available in older C# versions. Type patterns and var patterns came in C# 7.0. Relational patterns, or, and, and the not pattern arrived in C# 9.0. Property patterns existed in C# 8.0. If you are targeting an older language version, you must stick to type and var patterns. Check the LangVersion setting in your project file if you need to support a specific C# version.

When you use patterns, remember that the C# compiler may generate different IL depending on the target framework. For example, switch expressions are implemented as if-else chains or a jump table when possible. This is not a behavior you need to worry about, but it means that the cost of a switch expression is similar to a series of if-statements.

When to Use is Patterns Instead of Other Techniques

is patterns replace many traditional constructs: typeof checks, as casts, null checks, and if chains on property values. They are especially useful in scenarios where you need to branch on the shape of an object, such as in a parser, a message handler, or a validation routine.

Use a type pattern when you only need to test for a type. Use a property pattern when you need additional conditions but you want to keep the syntax concise. Use a switch expression when you have multiple possible branches and you want to return a value.

The alternative would be writing explicit if statements with is and and. In many cases, a property pattern is no less readable than the equivalent if-statement, and it keeps the condition together with the type check.

One particular case where is patterns are not appropriate is when you need to perform custom equality or comparison that is not based on property values. For example, if you need to compare two objects using a custom IEqualityComparer, pattern matching will not help. In those cases, stick to conventional methods.

Nested Patterns and Recursion

Patterns can be nested arbitrarily. You can combine property patterns with positional patterns (using tuples) and and/or patterns. For example:

static bool IsPointInFirstQuadrant(object obj) => obj is Point { X: > 0, Y: > 0 };

Or with a tuple pattern:

static string Classify((int X, int Y) point) => point switch { (0, 0) => "origin", (> 0, > 0) => "first quadrant", (< 0, > 0) => "second quadrant", // ... _ => "other" };

These combined patterns are powerful, but they can become hard to read if overused. Keep the pattern depth to a level that a teammate can parse at a glance.

Conclusion

In summary, c# is pattern matching gives developers a syntax for expressing type checks and structural tests in a declarative way. The is keyword now supports type, property, relational, and logical patterns, and switch expressions provide a concise alternative to if-else chains. This reduces boilerplate and makes the intent of the code clearer, especially when dealing with complex conditions. The runtime cost is generally low, and the compiler can optimize. When you update to a modern C# language version, consider replacing verbose type checks and property checks with is patterns, but be mindful of the language version you target and the readability of deeply nested patterns.

c# is pattern matching: Practical Usage and Code Examples | RYUSLOG DEV