C# Pattern Matching with Records: Simplifying Complex Data Transformations
When Switch Statements Get Out of Hand
I remember refactoring a legacy order processing system where a single method had grown to over 300 lines, mostly filled with nested if-else chains checking order states, payment statuses, and customer tiers. It was brittle, hard to test, and every new requirement felt like defusing a bomb. That’s when I started leaning harder into C# pattern matching — not just the basic switch expressions, but combining them with records and positional deconstruction to turn complex conditional logic into something readable and maintainable.
The Problem: Branching Logic Hell
Consider a shipping calculator that needs to determine rates based on package weight, destination zone, and customer loyalty tier. A naive implementation might look like this:
public decimal CalculateShippingRate(Order order)
{
if (order.Weight > 50)
{
if (order.Destination == "International")
{
return order.IsPremiumCustomer ? 25.00m : 40.00m;
}
else
{
return order.IsPremiumCustomer ? 15.00m : 25.00m;
}
}
else
{
if (order.Destination == "International")
{
return order.IsPremiumCustomer ? 10.00m : 15.00m;
}
else
{
return order.IsPremiumCustomer ? 5.00m : 10.00m;
}
}
}
This works, but it’s painful to read, easy to break when modifying, and nearly impossible to unit test all paths without duplication. As requirements grow — say, adding holiday surcharges or regional overrides — this approach collapses under its own weight.
The Fix: Pattern Matching with Records
C# 9 introduced records, which are perfect for modeling data carriers like our order details. Combined with pattern matching switch expressions, we can replace nested conditionals with a clear, top-down decision tree that’s both exhaustive and easy to extend.
public record ShippingCriteria(double WeightKg, string DestinationZone, bool IsPremium);
public decimal CalculateShippingRate(Order order)
{
var criteria = new ShippingCriteria(
order.WeightKg,
order.DestinationZone,
order.IsPremiumCustomer
);
return criteria switch
{
// Heavy international shipments
{ WeightKg: > 50, DestinationZone: "International" } =>
order.IsPremiumCustomer ? 25.00m : 40.00m,
// Heavy domestic
{ WeightKg: > 50, DestinationZone: "Domestic" } =>
order.IsPremiumCustomer ? 15.00m : 25.00m,
// Light international
{ WeightKg: <= 50, DestinationZone: "International" } =>
order.IsPremiumCustomer ? 10.00m : 15.00m,
// Light domestic (fallback)
{ WeightKg: <= 50, DestinationZone: "Domestic" } =>
order.IsPremiumCustomer ? 5.00m : 10.00m,
// Catch-all for invalid zones — fail fast
{ DestinationZone: var zone } => throw new ArgumentException(
$