Yep - the switch statement in C# has evolved from a simple value-based branching construct into a pattern matching engine.
In early C# versions (1.0–6.0), switch worked only with constant values of limited types: integral types (int, byte, char, etc.), enum, and later string (added in C# 2.0). Each case had to be a compile-time constant, and pattern matching was not supported. Control flow relied on break, and fall-through was not allowed except for empty cases.
C# 7.0 introduced pattern matching. Switch could now match on type patterns and declaration patterns, for example matching an object by its runtime type. C# 7.3 improved pattern matching slightly, but the big shift came in C# 8.0 with switch expressions. Instead of a statement block, switch could return a value directly, making it expression-based and more functional in style. This enabled more concise and safer code, especially when combined with expression-bodied members.
C# 9.0 added relational patterns (<, >, <=, >=), logical patterns (and, or, not), and improved type patterns. C# 10 and C# 11 refined pattern matching with extended property patterns and required members. C# 12 and later versions (used in .NET 8–10 era) continued enhancing list patterns, slice patterns, and recursive patterns, allowing matching against arrays and collections structurally. By .NET 10 / modern C#, switch is essentially a full pattern matching system capable of handling complex object graphs.
Switch input is not limited to string. That was true only in older versions where supported types were restricted. Today you can switch on almost any type, including object, and use pattern matching to inspect types, properties, ranges, null, collections, and more. The original limitation existed because switch was compiled to jump tables and required constant comparisons. Pattern matching removed that restriction by lowering code to decision trees instead of simple jump tables.
In .NET 10, the recommendation is to prefer switch expressions when you are computing a value and switch statements when you need side effects or multiple statements per branch. You should use pattern matching instead of large if-else chains. You should also avoid default if you want exhaustiveness checking on enums, because the compiler can warn you when new enum values are added.
Classic switch statement (old style):
int number = 2;
switch (number)
{
case 1:
Console.WriteLine("One");
break;
case 2:
Console.WriteLine("Two");
break;
default:
Console.WriteLine("Other");
break;
}
Modern switch expression:
int number = 2;
string result = number switch
{
1 => "One",
2 => "Two",
_ => "Other"
};
Console.WriteLine(result);
Pattern matching with types:
object input = "Hello";
string message = input switch
{
string s => $"String of length {s.Length}",
int i => $"Integer value {i}",
null => "Null value",
_ => "Unknown type"
};
Relational and logical patterns:
int age = 25;
string category = age switch
{
< 13 => "Child",
>= 13 and < 20 => "Teenager",
>= 20 and < 65 => "Adult",
_ => "Senior"
};
Property pattern:
public record Person(string Name, int Age);
Person person = new("Alice", 30);
string description = person switch
{
{ Age: < 18 } => "Minor",
{ Age: >= 18 and < 65 } => "Working age",
{ Age: >= 65 } => "Retired"
};
List pattern (modern C#):
int[] numbers = { 1, 2, 3 };
string shape = numbers switch
{
[] => "Empty",
[1] => "Single 1",
[1, 2, ..] => "Starts with 1,2",
_ => "Other"
};
If the above response helps answer your question, remember to "Accept Answer" so that others in the community facing similar issues can easily find the solution. Your contribution is highly appreciated.
hth
Marcin