C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
14: Pattern Matching in Switch Expressions
I've seen this exact bug land in pull requests more times than I can count. A developer wants to clean up a messy if-else chain by using the newer C# switch expressions, but they forget one critical detail: the compiler is a lot more strict about "exhaustiveness" than it is with the old-school switch statements.
public decimal GetProcessingFee(PaymentMethod method) => method switch
{
PaymentMethod.CreditCard => 2.50m,
PaymentMethod.PayPal => 1.20m,
PaymentMethod.BankTransfer => 0.00m
};
The Crash You Didn't See Coming
On the surface, this looks clean. If PaymentMethod is an enum with those three values, it works perfectly. But imagine a teammate adds PaymentMethod.Crypto to the enum a month later. This code doesn't have a compile-time error—it compiles just fine. However, the moment a Crypto payment hits this method, the application will throw a SwitchExpressionException and crash.
The old switch statement just silently does nothing if no case matches. The switch expression, because it must return a value, cannot just "do nothing." If it reaches the end without a match, it panics. I always tell my juniors: if you're using a switch expression, you are making a contract with the compiler that every possible input is handled.
Closing the Gap with the Discard Pattern
To fix this, we use the discard pattern (the underscore _). This acts as the default case from the traditional switch statement. It ensures that no matter what happens to your data models in the future, the method always returns a value.
public decimal GetProcessingFee(PaymentMethod method) => method switch
{
PaymentMethod.CreditCard => 2.50m,
PaymentMethod.PayPal => 1.20m,
PaymentMethod.BankTransfer => 0.00m,
_ => throw new ArgumentOutOfRangeException(nameof(method), $"Unsupported payment method: {method}")
};
Now, instead of a generic SwitchExpressionException, we provide a meaningful error. Or, if a default fee makes sense, you could just return 5.00m. The point is, the _ ensures the expression is exhaustive.
Matching on More Than Just Values
The real power of this syntax isn't just replacing enums; it's pattern matching. You can check types, ranges, and properties all in one go. I personally find this much more readable than nesting three if statements inside each other.
Let's say we're calculating a shipping discount based on an Order object. We can match based on the property values directly inside the switch:
public decimal GetShippingDiscount(Order order) => order switch
{
{ TotalAmount: > 100 } => 10.00m, // Relational pattern
{ TotalAmount: > 50 } => 5.00m, // Relational pattern
{ Customer.IsPremium: true } => 2.00m, // Property pattern (deep dive)
_ => 0.00m // The safety net
};
Notice how I didn't have to write order.TotalAmount > 100. The { TotalAmount: > 100 } syntax tells C#: "If this object has a property called TotalAmount and it's greater than 100, use this arm." It's concise, but it keeps the logic focused on the data rather than the plumbing.
Adding Precision with Guard Clauses
Sometimes a simple property match isn't enough. You might need to call a method or perform a complex calculation to decide the result. That's where the when keyword comes in. Think of it as a filter for your pattern.
public string GetShippingPriority(Order order) => order switch
{
{ TotalAmount: > 500 } => "Overnight",
{ TotalAmount: > 100 } when order.IsInternational => "Priority Air",
{ TotalAmount: > 100 } => "Standard Ground",
_ => "Economy"
};
The order here is vital. C# evaluates these from top to bottom. If I put the "Standard Ground" case above the "Priority Air" case, the international check would never be reached because any amount over 100 would have already matched. Always put your most specific conditions at the top and your most general ones at the bottom.
📋 Practical Task
Refactoring the User Access Level Validator
You are working on a legacy permissions system. Currently, the code uses a clunky set of if/else statements to determine what a user can do based on their User object. Your task is to refactor this into a modern switch expression.
Requirements:
- If the user is an
Admin, they get"FullAccess". - If the user is a
ModeratorAND theirAccountAgeDaysis greater than 30, they get"ModeratedAccess". - If the user is a
Moderatorbut their account is 30 days old or younger, they get"LimitedModeratorAccess". - If the user is a
Guest, they get"ReadOnly". - Any other case (including nulls or undefined roles) should return
"NoAccess".
public class User
{
public string Role { get; set; } // "Admin", "Moderator", "Guest"
public int AccountAgeDays { get; set; }
}
public class AccessControl
{
public string GetPermissions(User user)
{
// TODO: Replace the logic below with a switch expression
if (user == null) return "NoAccess";
if (user.Role == "Admin") return "FullAccess";
if (user.Role == "Moderator")
{
if (user.AccountAgeDays > 30) return "ModeratedAccess";
return "LimitedModeratorAccess";
}
if (user.Role == "Guest") return "ReadOnly";
return "NoAccess";
}
}There are no comments for now.