C# Logical Pattern
c# logical pattern: Understand C# logical patterns and/or/not, how they combine with property and type patterns, and when they simplify data-validation code.
Modern C# pattern matching includes logical patterns built from the keywords and, or, and not. These operate like boolean logic applied to pattern evaluation, letting you express complex conditionals in a single concise expression. For example, if (temperature is > 0 and < 100) checks a range without compound && conditions. The c# logical pattern syntax appears in if statements, switch expressions, and switch statements, offering a readable way to combine multiple pattern clauses.
Logical Pattern Operators and Their Semantics
The three logical pattern combinators are and, or, and not. Their behavior follows standard boolean algebra, but they apply to pattern outcomes rather than boolean values.
andrequires both subpatterns to match.ormatches when at least one subpattern matches.notmatches when the following subpattern does not match.
These combinators can be nested and combined with other patterns, such as relational, property, type, and var patterns. The precedence rules mirror arithmetic-like expectations: not has the highest precedence, followed by and, then or. When in doubt, use parentheses to make the grouping explicit.
Consider a temperature classification example:
static string DescribeTemperature(int celsius) { return celsius switch { < 0 => "Freezing", >= 0 and < 20 => "Cool", >= 20 and < 30 => "Warm", >= 30 and < 40 => "Hot", >= 40 => "Extremely hot" }; }
Here each arm uses and to bind two relational patterns into a range check. This avoids chained && expressions and makes the range boundaries obvious at a glance.
Using and to Combine Relational or Type Patterns
and is the most common logical pattern. It is useful when a value must satisfy two independent conditions. For instance, validating that an integer is both positive and even:
static bool IsPositiveEven(int number) => number is > 0 and % 2 == 0;
Wait, the modulo operation is not directly supported inside a pattern. The example above is illustrative pseudo-pattern; in real code you would use a property pattern or a type check instead. A valid use is combining a type check and a property check:
if (shape is Rectangle { Width: > 0 and > 0 } r) { // r is a Rectangle with positive dimensions }
Actually, the and operator can combine two subpatterns that each may involve relational or property tests. Here is a correct example:
static bool IsValidCoordinate(int x, int y) => x is > -100 and < 100 && y is > -100 and < 100;
But you can also combine them on the same value:
if (point is (>= 0 and <= 100, >= 0 and <= 100)) { // point is within the 0..100 square }
The positional pattern applies and within each component. This keeps the range validation close to the data structure.
Using or to Match Multiple Alternatives
The or pattern allows a single condition to be true for multiple alternative shapes. For example, you might want to accept either URL or file path representations:
if (input is Uri or string path) { // input is either a Uri or a string }
But note that or patterns must bind the same variables? Actually, variable binding in or patterns is tricky. The variables declared in each alternative must have the same type, and they are not guaranteed to be definitely assigned after the match? In practice, or works best when you don't need to extract a value common to both branches. For instance, checking whether a value is null or empty:
if (value is null or "") { Console.WriteLine("Value is null or empty string"); }
Here the or pattern matches two literal patterns. This is clearer than value == null || value == "", especially when combined with other pattern features.
Using not for Negation
not is used to match when a pattern does not match. This is especially handy for guarding against a specific case before processing. For instance, checking that a string is not empty:
static bool HasContent(string? input) => input is not null and not "";
Here not null and not "" are combined with and. The not pattern can also be used in a switch expression to define a fallback arm:
string Size(double length) => length switch { > 100 => "Large", > 50 => "Medium", not > 0 => "Non-positive", _ => "Small" };
In this snippet, not > 0 catches zero or negative values. It is equivalent to <= 0, but the not form may be more readable when you want to negate a positive condition.
Combining Logical Patterns with Property and List Patterns
The power of logical patterns increases when you combine them with property patterns, list patterns, and positional patterns. For example, you might want to validate an object that has several required properties:
record Product(string Name, decimal Price, bool InStock); bool IsDeal(Product p) => p is { Price: <= 20, InStock: true };
But you can also use logical patterns inside property patterns to express thresholds:
bool IsUrgent(Order o) => o is { Total: > 1000 or < 0 };
This matches orders with a total over 1000 or a negative total (which might indicate an error). The or pattern is nested inside the property pattern, making the condition precise.
List patterns also integrate with logical patterns. For example, matching a sequence that starts with a specific element or is empty:
string FirstOrNone(int[] values) => values switch { [] => "Empty", [var first, ..] when first is > 0 => "Positive first", [var first, ..] when first is < 0 => "Negative first", [var first, ..] => "Zero first" };
Here the when clause uses relational patterns, but you could also use logical patterns inside the when clause: when first is > 0 and < 100.
When Logical Patterns Reduce Readability
Logical patterns are concise but can become dense. Overusing not with complex subpatterns can reduce clarity. For instance:
if (x is not (not > 0 and not < 100))
This double negation is nearly impossible to read at a glance. Prefer the straightforward relational pattern: x is <= 0 or >= 100. The goal of logical patterns is to make conditions clearer, not to pack as many operators as possible into one line.
Also, note that not cannot be used to negate a pattern that declares variables? Actually, not patterns cannot bind variables. This means you cannot write x is not int i to capture a non-int value. Instead, you must use a separate if or switch arm.
Performance and Maintainability Considerations
Logical patterns are evaluated at runtime like any other pattern. The C# compiler may optimize some simple patterns, such as and for numeric ranges, into efficient comparisons. However, you should not expect measurable performance differences between using logical patterns and equivalent &&/|| expressions—the compiler often emits similar IL.
More important is the maintainability benefit. Logical patterns keep decision logic in one place, especially when combined with switch expressions. For example, instead of a long chain of if-else if blocks, you can use a switch expression with clear arms:
static decimal ApplyDiscount(Order order) => order switch { { Total: > 1000 and < 5000 } => 0.1m, { Total: >= 5000 } => 0.15m, _ => 0m };
This structure is easier to review and modify because each arm is a self-contained condition that maps to a result.
When using logical patterns in a production system, consider that they are part of the language syntax. You need a C# compiler that supports them—essentially C# 9 or later. If you are working in a mixed-version codebase, verify the target framework compiles with the correct language version.
Compatibility and Language Version Support
Logical patterns were introduced in C# 9. If you are using an older language version, you must use traditional && and ||. Most modern .NET projects target C# 9 or later, so this is rarely a constraint, but when editing legacy code, be aware that the compiler may reject logical patterns without the appropriate <LangVersion> setting.
Here is a quick mapping of logical pattern usage to C# versions:
| Pattern | Introduced in | Example |
|---|---|---|
Logical and | C# 9 | a is > 0 and < 10 |
Logical or | C# 9 | a is null or "" |
Logical not | C# 9 | a is not null |
| Extended property patterns | C# 10 | obj is { A.B: > 0 } |
Keep this in mind when explaining code to teammates or when adopting patterns from more recent C# versions.
Using Logical Patterns to Replace Complex Guard Clauses
A common refactoring opportunity is replacing multiple nested if checks with a single logical pattern. For example, a guard that validates a request object:
if (request is { User: not null, Permissions: var perms } && perms.Contains("admin")) { // allowed }
With logical patterns, you can combine the null check and permission check as:
if (request is { User: not null, Permissions: { } perms } && perms.Contains("admin"))
This still uses &&, but the pattern ensures User is non-null. The point is that logical patterns are a tool, not a replacement for all code structure. Use them where they remove noise, not where they obscure logic.
A more direct replacement is when you have a series of && checks that all apply to the same variable. For example:
if (score >= 0 && score <= 10 && !isOverridden)
Could be rewritten as:
if (score is >= 0 and <= 10 && !isOverridden)
This groups the range constraints on score into a single pattern, making the condition's intent clearer. It is a small change, but over a large codebase, such simplification improves readability.
Common Mistakes with Logical Patterns
One mistake is using and where or is intended, or misplacing not. For example, x is not > 0 and < 10 is parsed as (x is not > 0) and (x < 10)? Actually, due to precedence, not binds tighter than and, so it becomes (x is not > 0) and (x is < 10). This may not be what you want. Always parenthesize when combining not with and/or.
Another mistake is trying to bind a variable inside a not pattern. The following is invalid:
if (x is not int i) // compiler error
You cannot declare i in a not pattern because the negation means i may not be assigned. Use a separate check instead.
Finally, remember that logical patterns do not short-circuit variable declarations. In an or pattern, both alternatives must declare the same variables with the same types, and those variables are only definitely assigned after a successful match if both paths assign them? Actually, in an or pattern, you cannot declare a variable at all? Let's verify: C# allows variable declarations only in patterns that guarantee assignment, such as var patterns or property patterns. In or, you can declare variables in each alternative, but they must have the same type? The compiler requires that variable patterns in or branches have the same type, and the variable is assigned if the overall pattern matches? The C# spec says that in an or pattern, the variables declared in each subpattern are considered to be in scope after the pattern, but they are only definitely assigned if they are assigned by whichever subpattern matched? Actually, the rule is that each subpattern must declare exactly the same set of variables with the same types, and those variables are definitely assigned after a successful match. For example:
if (x is int a or string s)
This is invalid because a and s have different types. To use such a pattern, you'd need a common type, like object:
if (x is int a or double a2)
The variables a and a2 are different, so the rule fails. The practical takeaway is that or patterns are best used when you don't need to extract a value from the matched alternative. If you need the value, use a var pattern or a type test with a separate guard.
Writing a Small Validation Utility with Logical Patterns
To see how logical patterns work together, consider a function that validates a user input string for a command line tool:
public static bool ValidateInput(string input) { return input switch { null or "" => false, { Length: > 100 } => false, not { } => false, _ => input.All(char.IsLetterOrDigit) }; }
In this switch expression, the first arm uses or to reject null or empty. The second arm uses a property pattern to reject strings longer than 100. The third arm not { } matches any non-null reference type? Actually it matches when the input is not a non-null string? Let's simplify: The { } pattern matches any non-null value of the type. not { } matches null. So the third arm is redundant with the first arm? The first arm already matches null, so not { } would never be reached. Here is a cleaner version:
public static bool ValidateInput(string input) { return input switch { null or "" => false, { Length: > 100 } => false, _ => input.All(char.IsLetterOrDigit) }; }
This works because the first arm catches both null and empty. The property pattern { Length: > 100 } is evaluated only when the string is not null. This demonstrates how logical patterns combine with property patterns to express validation rules compactly.
Final Technical Note: Using Logical Patterns in Recursive Patterns
Logical patterns can be nested inside recursive patterns, such as positional or list patterns. For instance, matching a point in the first quadrant of a coordinate system:
if (point is (>= 0 and <= 100, >= 0 and <= 100)) { // point is within the square }
This position-aware pattern is evaluated against a tuple or a type with Deconstruct. It reads as "x is between 0 and 100 AND y is between 0 and 100." This is a natural fit for geometric or grid-based logic.
When combining recursive patterns with logical patterns, keep an eye on readability. If the condition becomes too complex, extract it into a method with a descriptive name. Logical patterns are most valuable when they make the decision logic evident at the call site, not when they turn a single expression into a jumble of symbols.