TL;DR: C# pattern matching simplifies conditionals with property, relational, logical, and nested patterns. Write clearer code, reduce boilerplate, and safely match data shapes with switch, when, and exhaustive patterns.

If you’ve been writing C# for a while, you probably remember the days of building complex business logic with deeply nested if-else chains and clunky switch statements. It worked, but it often led to code that was hard to read and even harder to maintain. While pattern matching isn’t new, the enhancements in C# 9 and 10 have transformed it from a simple type-checking tool into a powerful, declarative language feature.

Getting fluent with modern patterns, property, logical, and relational, will fundamentally change how you write conditional logic. Your code will become more expressive, concise, and focused on the shape of your data rather than the imperative steps to inspect it. Let’s dive into some practical tricks.

The Evolution of is: More Than Just a Type Check

You’ll often see the is keyword used for a simple type check. But since C# 9, it’s become a powerhouse for checking an object’s properties in a single, clean expression. This is called a property pattern.

Imagine you have a method to validate an incoming API request DTO.

The Old Way: A Chain of Checks

public bool IsValidRequest(object request)
{
    if (request is CreateUserRequest dto)
    {
        // Now check the properties...
        if (dto.Username != null && dto.Password.Length > 8 && !dto.IsDisabled)
        {
            return true;
        }
    }
    return false;
}

public record CreateUserRequest(string Username, string Password, bool IsDisabled);

This is okay, but it’s a nested mess. The logic is spread across multiple lines and conditional blocks.

The Modern Way: A Declarative Property Pattern

With property patterns, you can do this in one line. Notice how clean this gets:

public bool IsValidRequest(object request)
{
    // Check type and properties in one go
    return request is CreateUserRequest { 
        Username: not null, 
        Password.Length: > 8, 
        IsDisabled: false 
    };
}

public record CreateUserRequest(string Username, string Password, bool IsDisabled);

This works because we’re describing the shape of a valid object directly in the is expression. We’re not just checking the type; we’re simultaneously checking its properties using relational patterns (> 8) and a negated constant pattern (not null).

Supercharging switch Expressions

The switch expression (introduced in C# 8) was already a huge improvement over the switch statement. C# 9+ makes it even better by allowing all these new pattern types inside its arms. This is perfect for processing different kinds of events from a message queue or handling API responses.

Let’s say we’re processing HttpResponseMessage objects.

The Old Way: switch Statement with if

public string ProcessResponse(HttpResponseMessage response)
{
    switch (response.StatusCode)
    {
        case System.Net.HttpStatusCode.OK:
            return "Success!";
        case System.Net.HttpStatusCode.Unauthorized:
        case System.Net.HttpStatusCode.Forbidden:
            return "Permission denied.";
        case System.Net.HttpStatusCode.InternalServerError:
            if (response.RequestMessage.Method == HttpMethod.Post)
            {
                return "Critical failure during a POST operation.";
            }
            return "A server error occurred.";
        default:
            return "Some other response.";
    }
}

The Modern Way: switch Expression with Nested Patterns

The switch expression lets us combine all of this into a much more readable and declarative form. We can even use nested property patterns to inspect the RequestMessage.

public string ProcessResponse(HttpResponseMessage response) => response switch
{
    // Relational and logical patterns for status codes
    { StatusCode: >= System.Net.HttpStatusCode.OK and < System.Net.HttpStatusCode.MultipleChoices } 
        => "Success!",

    // 'or' pattern for multiple status codes
    { StatusCode: System.Net.HttpStatusCode.Unauthorized or System.Net.HttpStatusCode.Forbidden } 
        => "Permission denied.",

    // Nested property pattern with a guard
    { StatusCode: System.Net.HttpStatusCode.InternalServerError, 
      RequestMessage: { Method: { Method: "POST" } } } 
        => "Critical failure during a POST operation.",

    // Catch-all for other server errors
    { IsSuccessStatusCode: false } => "A generic failure occurred.",
    
    _ => "Some other response."
};

Notice how we use and, or, and relational operators directly in the case arms. The code reads like a series of rules rather than a procedural checklist.

Fine-Tuning with the when Clause

Sometimes, a pattern can’t express a condition on its own, especially if it involves calling a method or accessing something outside the object being matched. That’s where the when clause comes in. It acts as an additional filter, or a “guard,” that runs only if the main pattern matches.

A great use case is checking user permissions that require a service call.

public string GetDashboardUrl(User user) => user switch
{
    // The 'when' clause is a guard that runs after the pattern matches
    { IsAdmin: true } => "/admin/dashboard",
    
    // Check the pattern first, then evaluate the 'when' clause
    { IsEmployee: true } u when _permissionService.HasPremiumAccess(u.Id) 
        => "/employee/premium-dashboard",

    { IsEmployee: true } => "/employee/basic-dashboard",

    _ => "/login"
};

// Dummy service for demonstration
private readonly IPermissionService _permissionService = new PermissionService();

This trick helps when your logic is too complex for a simple property check. The user object is matched first, and only then is HasPremiumAccess() called. This avoids running expensive checks on objects that don’t even match the basic pattern.

Summary Table of Modern Patterns

Here’s a quick reference table to help you remember the different pattern types and where to use them.

Pattern TypeSyntax ExampleCommon Use Case
Property Pattern{ Name: "John", Age: > 30 }Checking multiple properties of an object in one expression.
Relational Pattern> 100, <= 50Comparing numerical or ordered values.
Logical Pattern> 10 and < 20, not null, "A" or "B"Combining other patterns with boolean logic.
Parenthesized(> 10 and < 20) or 50Grouping patterns to control precedence explicitly.
Nested Pattern{ Order: { Amount: > 1000 } }Matching against properties of a nested object.
when ClauseUser u when u.IsActive()Applying a complex, external condition to a pattern.

Common Mistakes and Gotchas

  1. when Isn’t a Short-Circuit: The main pattern is always evaluated before the when clause. Don’t rely on when to prevent a NullReferenceException if the pattern itself could fail on a null object.
  2. switch Expression Exhaustiveness: The compiler forces switch expressions to be exhaustive, meaning all possible inputs must be handled. This is a feature, not a bug! Use the discard _ for a catch-all case.
  3. Order Matters: In a switch expression, the first matching arm wins. Place more specific patterns before more general ones. A { IsSuccessStatusCode: false } would catch a 500 error before a more specific { StatusCode: 500, ... } if placed first.

Takeaway: A New Mental Model for Conditionals

The best way to start using these features is to look for code you’ve already written. Find those long if-else if chains or switch statements with lots of internal logic. You’ll be surprised how many can be refactored into a single, elegant switch expression.

Think of modern pattern matching as a form of smart, conditional destructuring. You’re not just checking a type; you’re declaring the shape and state of the data you expect, pulling out the pieces you need, and evaluating them all in one cohesive, readable block. It’s one of the most powerful tools in modern C# for writing clean, maintainable, and declarative code.

FAQ

What is property pattern matching in C# and why is it useful?

Property pattern matching allows you to check an object’s type and its properties in a single expression. This makes code more concise and expressive, especially when validating DTOs or filtering data.

How do relational and logical patterns simplify conditional checks?

Relational patterns let you compare values directly in patterns, such as > 10 or <= 50. This reduces the need for nested if statements and makes your intent clearer.

What role do logical operators like and, or, and not play in pattern matching?

Logical patterns like and, or, and not allow you to combine multiple conditions in a single pattern. This enables more readable and maintainable code compared to chaining multiple checks.

How do you use the when clause to add conditions to patterns?

The when clause acts as a guard, applying an additional condition after the main pattern matches. It’s useful for complex checks that can’t be expressed directly in the pattern, such as method calls or external service checks.

What are nested patterns in C# and when should you apply them?

Nested patterns allow you to match properties of properties, enabling deep inspection of complex objects. Use them when you need to validate or process data structures with multiple levels.

Why is the order of patterns important in switch expressions?

The first matching pattern wins in a switch expression. Place more specific patterns before general ones to ensure the correct logic is applied.

How does the compiler ensure that switch expressions are exhaustive?

The compiler requires that all possible input cases are handled in a switch expression. Use the discard _ pattern as a catch-all to ensure your code is exhaustive and safe.

What are common pitfalls to avoid with pattern matching in C#?

Common mistakes include relying on the when clause for null checks (the main pattern runs first), not handling all cases in a switch expression, and placing general patterns before specific ones, which can cause logic errors.

How does pattern matching make C# code more maintainable?

Pattern matching reduces boilerplate, clarifies intent, and centralizes conditional logic. This makes code easier to read, test, and modify, especially in large codebases.

Does pattern matching replace all if-else and switch statements in C#?

While pattern matching covers many scenarios, some complex logic may still require traditional conditionals. However, most data shape and value checks can be refactored to use modern patterns.

Related Posts