c# relational pattern
Learn how to use c# relational pattern to write concise, readable comparison logic in switch expressions and property patterns.
In C#, relational patterns let you compare an input value against a constant using operators like <, >, <=, and >= inside a pattern. The c# relational pattern is a core part of the pattern-matching toolkit introduced incrementally from C# 9 onward. It is especially useful in switch expressions and property patterns where you want to express range checks without writing verbose if chains.
Syntax and Placement
A relational pattern uses one of the relational operators followed by a constant expression. The pattern is placed where a pattern is expected, such as in a switch expression arm or after the when clause in a switch statement.
static string Classify(int value) => value switch { < 0 => "negative", 0 => "zero", > 0 and <= 10 => "small positive", > 10 => "large positive", _ => "unknown" };
The constant must be a compile-time constant, such as a numeric literal, an enum member, or a const variable. You cannot use a non-constant variable directly in a relational pattern; for dynamic thresholds you need a when clause with a regular comparison.
Combining Relational Patterns with Logical Patterns
Relational patterns are often combined with logical patterns using and and or. The and operator combines two patterns, requiring both to match. The or operator requires either to match. The not operator negates a pattern.
static string TemperatureDescription(double temp) => temp switch { < 0 => "freezing", >= 0 and < 15 => "cold", >= 15 and <= 25 => "mild", > 25 and < 35 => "warm", >= 35 => "hot", _ => "unknown" };
The order of arms matters. The first matching arm produces the result. Overlapping ranges can lead to unreachable arms, so arrange thresholds from most specific to most general. In the preceding example, >= 0 and < 15 and >= 15 and <= 25 do not overlap, but if you wrote >= 0 and <= 15 and >= 15 you would create an overlap at exactly 15, and the second arm would never match for 15.
Using Relational Patterns in Property Patterns
Relational patterns are not limited to switching directly on a value. They also work inside property patterns, where you check properties of an object. This is helpful when you want to validate a range on a property without extracting it first.
record TemperatureReading(double Celsius, DateTime Timestamp); static string Categorize(TemperatureReading reading) => reading switch { { Celsius: < 0 } => "freezing", { Celsius: >= 0 and < 15 } => "cold", { Celsius: >= 15 and <= 25 } => "mild", { Celsius: > 25 } => "warm", _ => "unknown" };
The property pattern accesses Celsius and applies a relational pattern to its value. The compiler emits a direct property access, so there is no dynamic lookup cost.
Relational Patterns in when Clauses
In a traditional switch statement, you can use a when clause to apply a relational condition. This is more verbose than a switch expression but offers additional flexibility because the condition can be any boolean expression.
switch (score) { case int n when n < 50: Console.WriteLine("Fail"); break; case int n when n >= 50 and n < 70: Console.WriteLine("Pass"); break; case int n when n >= 70: Console.WriteLine("Distinction"); break; }
The when clause supports relational operators directly, so you can write when n >= 50 and n < 70 without needing a relational pattern. However, using relational patterns inside a property pattern or switch expression often yields more concise code, especially when the pattern is nested.
Common Mistakes and Edge Cases
One frequent error is using a relational pattern with a variable instead of a constant. For example, > minValue where minValue is a local variable will not compile. The compiler requires a constant expression. If you need to compare against a runtime value, use a when clause.
Another edge case is the NaN value for floating-point types. Comparison with NaN is always false, so relational patterns involving NaN will not match. For example, double.NaN does not satisfy < 0, >= 0, or any other relational pattern. If you must handle NaN explicitly, add a pattern like double.NaN or use a when clause with double.IsNaN.
Relational patterns work with types that implement comparison operators. For numeric types and enums this is straightforward. For custom types, the pattern uses the underlying comparison logic, but the compiler requires the type to have an appropriate operator. A type that does not define > or < cannot be used directly in a relational pattern.
Performance Considerations
Relational patterns are evaluated by the compiler as a series of comparisons. For a linear chain of arms, the compiler might generate a jump table or a binary search depending on the pattern. The exact compilation depends on the shape of the pattern and the target runtime. You should not assume a specific internal algorithm, but the overhead is generally small for a handful of comparisons.
In hot paths, avoid placing a relational pattern inside a deep object graph if a simple if would be clearer. The compiled code for a relational pattern is similar to an explicit comparison, so there is no magic performance gain. The benefit is readability and maintainability, not raw speed.
Compatibility and Language Version
Relational patterns require C# 9 or later. The and, or, and not logical patterns also require C# 9. If you are working on an older codebase, you might be restricted to C# 8 where relational patterns are not available. In C# 8, you could use when clauses with traditional switch statements, but switch expressions with relational patterns were not valid.
Ensure your project targets a recent .NET runtime or at least a LangVersion that supports C# 9. For .NET Framework projects, you may need to set the language version explicitly. The pattern features are compile-time constructs, so the runtime does not need special support, but the compiler does.
Nested Relational Patterns and Type Patterns
Relational patterns can be combined with type patterns to handle different input types in a single switch. For example, you can switch on an object and apply a type pattern that includes a relational check on a property.
static string DescribeShape(object shape) => shape switch { Circle { Radius: < 0 } => "invalid circle", Circle { Radius: > 0 and <= 5 } => "small circle", Circle { Radius: > 5 } => "large circle", Rectangle { Width: > 0, Height: > 0 } => "valid rectangle", _ => "unknown" }; record Circle(double Radius); record Rectangle(double Width, double Height);
Here, the property pattern { Radius: < 0 } applies a relational pattern to the Radius property after checking the type is Circle. Combining type and property patterns lets you write compact polymorphic logic that would otherwise require multiple if statements with casts.
When to Prefer a when Clause Over a Relational Pattern
A relational pattern works when the threshold is a constant. If the threshold depends on user input, configuration, or another runtime value, you cannot express it with a relational pattern. In that situation, use a when clause or a regular if statement.
static string Classify(int value, int min, int max) => value switch { _ when value < min => "below", _ when value > max => "above", _ => "in range" };
This example uses when because min and max are parameters. The pattern _ matches any value, and the when clause applies the dynamic comparison. This approach keeps the switch expression readable while allowing variable thresholds.