Using Relational Patterns for Range Checks in C

Pattern matching in modern C# significantly improves code clarity, and the introduction of relational patterns provides an elegant way to evaluate numerical boundaries. This article explores how relational patterns streamline range validation inside switch expressions, comparing traditional verbose approaches with the modern syntax that combines relational and logical patterns for cleaner, more maintainable code.

The Problem with Traditional Range Checking

Before relational patterns were introduced in C# 9, evaluating ranges inside switch constructs required verbose code. Developers typically relied on extensive if-else chains or switch expressions packed with repetitive when guard clauses:

// Traditional approach using 'when' clauses
string GetTemperatureDescription(int temp) => temp switch
{
    int t when t < 0 => "Freezing",
    int t when t >= 0 && t <= 15 => "Cold",
    int t when t > 15 && t <= 25 => "Comfortable",
    _ => "Hot"
};

This pattern required declaring a discarded variable (t) in every arm and repeating the comparison target, obscuring the actual business logic behind syntactic noise.

Simplifying with Relational Patterns

Relational patterns allow you to use standard comparison operators—<, <=, >, and >=—directly against the input value without declaring a temporary variable or using the when keyword.

Here is the previous example rewritten using relational patterns:

// Modern approach using relational and logical patterns
string GetTemperatureDescription(int temp) => temp switch
{
    < 0 => "Freezing",
    >= 0 and <= 15 => "Cold",
    > 15 and <= 25 => "Comfortable",
    _ => "Hot"
};

Combining Relational and Logical Patterns

Relational patterns become especially effective when combined with the logical pattern combinators and, or, and not. Instead of relying on C#'s traditional binary operators (&&, ||, !), switch expressions use readable keywords to create expressive boundaries:

string GetGrade(double score) => score switch
{
    < 0 or > 100 => throw new ArgumentOutOfRangeException(nameof(score)),
    >= 90 => "A",
    >= 80 and < 90 => "B",
    >= 70 and < 80 => "C",
    >= 60 and < 70 => "D",
    _ => "F"
};

Key Advantages

  1. Elimination of Boilerplate: You avoid naming temporary variables or writing redundant references to the tested expression.
  2. Improved Readability: Range checks read like mathematical interval notations, making business rules immediately clear at a glance.
  3. Compiler Diagnostics: The C# compiler can evaluate pattern combinations more effectively, warning you about unreachable switch arms or overlapping patterns that will never execute.