Overview
Switch expressions in C# are a modern, expression-based way to choose and return a value based on pattern matching. They are commonly used when code needs to convert one value into another, classify data, map enum values, handle simple business rules, or replace long if/else chains with clearer and more compact logic.
A traditional switch statement executes statements. A switch expression evaluates to a value. This difference is important because switch expressions are often used directly in assignments, return statements, expression-bodied members, LINQ projections, DTO mapping, validation logic, and domain rule classification.
Switch expressions matter because they make decision logic easier to read when each branch produces a result. They also integrate deeply with C# pattern matching, which means they can match by value, type, property shape, tuple values, relational conditions, list patterns, and more.
For interviews, this topic is important because it tests several practical C# skills at the same time:
- Understanding the difference between expressions and statements
- Knowing modern C# syntax
- Applying pattern matching correctly
- Writing readable conditional logic
- Avoiding null-related and non-exhaustive switch bugs
- Choosing between
switch,if/else, polymorphism, dictionaries, and strategy patterns - Recognizing when compact syntax improves code and when it hides complexity
A good candidate should be able to explain not only the syntax, but also when switch expressions are appropriate, how arm ordering works, how exhaustive matching works, and how to use patterns without making business rules difficult to maintain.
Core Concepts
What a Switch Expression Is
A switch expression evaluates an input expression against multiple arms and returns the value from the first matching arm.
Basic syntax:
var result = input switch
{
pattern1 => value1,
pattern2 => value2,
_ => defaultValue
};
A switch expression contains:
- An input expression before the
switchkeyword - One or more switch arms
- A pattern for each arm
- An optional
whenguard - The
=>token - A result expression
- Commas between arms
- A semicolon after the full expression when used as a statement
Example:
public enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}
public static string GetStatusLabel(OrderStatus status)
{
return status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Submitted => "Submitted",
OrderStatus.Paid => "Paid",
OrderStatus.Cancelled => "Cancelled",
_ => "Unknown"
};
}
This is useful because the method clearly says: "given a status, return a label."
Switch Expression vs Switch Statement
A switch statement executes code blocks. A switch expression returns a value.
Switch statement example:
string label;
switch (status)
{
case OrderStatus.Draft:
label = "Draft";
break;
case OrderStatus.Paid:
label = "Paid";
break;
default:
label = "Unknown";
break;
}
Switch expression equivalent:
string label = status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Paid => "Paid",
_ => "Unknown"
};
Important differences:
Use a switch expression when every branch naturally produces a result. Use a switch statement when each branch performs multiple actions, mutates state, logs several things, calls multiple services, or needs more imperative flow.
Switch Arms and Evaluation Order
Each branch in a switch expression is called a switch arm.
var message = score switch
{
>= 90 => "Excellent",
>= 75 => "Good",
>= 50 => "Pass",
_ => "Fail"
};
Switch arms are evaluated from top to bottom. The first matching arm is selected.
Order matters. For example:
var message = score switch
{
>= 50 => "Pass",
>= 90 => "Excellent",
_ => "Fail"
};
In this version, >= 90 is never reached because values greater than or equal to 90 also match >= 50 first. The compiler can detect some unreachable arms, but developers should still order patterns from most specific to most general.
Best practice:
- Put special cases first
- Put broader cases later
- Put
_or catch-all cases last - Avoid overlapping conditions unless the order is intentional and obvious
The Discard Pattern _
The discard pattern _ matches anything that has not already matched.
public static string GetRoleName(string role)
{
return role switch
{
"admin" => "Administrator",
"manager" => "Manager",
"user" => "Standard User",
_ => "Unknown Role"
};
}
The _ arm is similar to default in a traditional switch statement.
It is often used to make the switch expression exhaustive. Without a catch-all arm, a switch expression may fail at runtime if no pattern matches.
Exhaustiveness and Runtime Exceptions
A switch expression should handle all possible input values. If no arm matches, the runtime throws an exception.
Example of a risky switch expression:
public static string GetPriorityLabel(int priority)
{
return priority switch
{
1 => "Low",
2 => "Medium",
3 => "High"
};
}
If priority is 4, no arm matches.
Safer version:
public static string GetPriorityLabel(int priority)
{
return priority switch
{
1 => "Low",
2 => "Medium",
3 => "High",
_ => "Unknown"
};
}
In business code, deciding whether to use _ depends on the scenario.
Use _ to provide a safe fallback:
public static string GetDisplayName(OrderStatus status)
{
return status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Submitted => "Submitted",
OrderStatus.Paid => "Paid",
OrderStatus.Cancelled => "Cancelled",
_ => "Unknown"
};
}
Throw an exception when an unexpected value indicates a programming or data integrity problem:
public static bool CanBeCancelled(OrderStatus status)
{
return status switch
{
OrderStatus.Draft => true,
OrderStatus.Submitted => true,
OrderStatus.Paid => false,
OrderStatus.Cancelled => false,
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unsupported order status.")
};
}
For interviews, it is important to explain that _ is not always the best answer. Sometimes a fallback is correct. Sometimes failing fast is better.
Constant Patterns
A constant pattern matches a specific value.
public static decimal GetDiscountRate(string customerType)
{
return customerType switch
{
"VIP" => 0.20m,
"Member" => 0.10m,
"Guest" => 0.00m,
_ => 0.00m
};
}
Constant patterns are commonly used with:
- Enums
- Strings
- Integers
- Booleans
- Known code values
Example with enum:
public static int GetSortOrder(OrderStatus status)
{
return status switch
{
OrderStatus.Draft => 1,
OrderStatus.Submitted => 2,
OrderStatus.Paid => 3,
OrderStatus.Cancelled => 4,
_ => int.MaxValue
};
}
Relational Patterns
Relational patterns compare the input value using operators such as <, <=, >, and >=.
public static string ClassifyAge(int age)
{
return age switch
{
< 0 => "Invalid",
< 13 => "Child",
< 20 => "Teenager",
< 65 => "Adult",
_ => "Senior"
};
}
Relational patterns are useful for:
- Range classification
- Score grading
- Age grouping
- Quantity thresholds
- Pricing rules
- Risk categories
A common mistake is ordering ranges incorrectly.
Bad example:
public static string ClassifyScore(int score)
{
return score switch
{
>= 50 => "Pass",
>= 90 => "Excellent",
_ => "Fail"
};
}
Correct version:
public static string ClassifyScore(int score)
{
return score switch
{
>= 90 => "Excellent",
>= 75 => "Good",
>= 50 => "Pass",
_ => "Fail"
};
}
Logical Patterns: and, or, and not
Logical patterns combine other patterns.
public static string ClassifyTemperature(decimal temperature)
{
return temperature switch
{
< 0 => "Freezing",
>= 0 and < 20 => "Cold",
>= 20 and < 30 => "Warm",
>= 30 => "Hot"
};
}
The or pattern can group multiple values:
public static bool IsWeekend(DayOfWeek day)
{
return day switch
{
DayOfWeek.Saturday or DayOfWeek.Sunday => true,
_ => false
};
}
The not pattern can express exclusions:
public static bool IsValidName(string? name)
{
return name switch
{
not null and not "" => true,
_ => false
};
}
For many real-world cases, simpler code may be more readable:
public static bool IsValidName(string? name)
{
return !string.IsNullOrWhiteSpace(name);
}
Best practice: use logical patterns when they make the business rule easier to read, not just because the syntax is available.
Type and Declaration Patterns
A switch expression can match based on runtime type and declare a variable.
public static string DescribeObject(object? value)
{
return value switch
{
null => "Null value",
int number => $"Integer: {number}",
string text => $"String with length {text.Length}",
DateTime date => $"Date: {date:yyyy-MM-dd}",
_ => "Unknown object"
};
}
This is useful when handling values with different possible runtime types.
Example in application code:
public interface IDomainEvent;
public sealed record OrderCreated(Guid OrderId) : IDomainEvent;
public sealed record OrderCancelled(Guid OrderId, string Reason) : IDomainEvent;
public static string GetEventDescription(IDomainEvent domainEvent)
{
return domainEvent switch
{
OrderCreated e => $"Order created: {e.OrderId}",
OrderCancelled e => $"Order cancelled: {e.OrderId}. Reason: {e.Reason}",
_ => "Unknown event"
};
}
However, too much type switching may be a design smell. If each type has its own behavior, polymorphism or the strategy pattern may be better.
Property Patterns
Property patterns match based on object property values.
public sealed class Order
{
public decimal TotalAmount { get; init; }
public bool IsPaid { get; init; }
public bool IsCancelled { get; init; }
}
public static string GetOrderCategory(Order order)
{
return order switch
{
{ IsCancelled: true } => "Cancelled",
{ IsPaid: false } => "Pending Payment",
{ TotalAmount: >= 1000 } => "High Value",
_ => "Standard"
};
}
Property patterns are useful for business rules that depend on object state.
They can also be nested:
public sealed class Customer
{
public string Type { get; init; } = "";
}
public sealed class Order
{
public Customer Customer { get; init; } = new();
public decimal TotalAmount { get; init; }
}
public static decimal GetDiscount(Order order)
{
return order switch
{
{ Customer.Type: "VIP", TotalAmount: >= 500 } => 0.20m,
{ Customer.Type: "VIP" } => 0.10m,
{ TotalAmount: >= 1000 } => 0.05m,
_ => 0.00m
};
}
Property patterns can improve readability when the object shape is simple. They can become hard to maintain when the business rule is complex, deeply nested, or frequently changing.
Positional Patterns and Deconstruction
Positional patterns work with types that support deconstruction, including tuples and records.
public readonly record struct Point(int X, int Y);
public static string DescribePoint(Point point)
{
return point switch
{
(0, 0) => "Origin",
(0, _) => "On Y axis",
(_, 0) => "On X axis",
(> 0, > 0) => "Quadrant I",
(< 0, > 0) => "Quadrant II",
(< 0, < 0) => "Quadrant III",
(> 0, < 0) => "Quadrant IV"
};
}
This works because the record struct provides deconstruction support.
Positional patterns are useful when the meaning of positions is obvious. For complex domain objects, property patterns are often clearer because property names communicate intent.
Compare:
// Less clear if the tuple positions are not obvious
var result = (order.TotalAmount, order.IsPaid, order.IsCancelled) switch
{
(_, _, true) => "Cancelled",
(_, false, _) => "Pending Payment",
(>= 1000, true, false) => "High Value",
_ => "Standard"
};
With:
// Clearer because property names are visible
var result = order switch
{
{ IsCancelled: true } => "Cancelled",
{ IsPaid: false } => "Pending Payment",
{ TotalAmount: >= 1000 } => "High Value",
_ => "Standard"
};
Tuple Patterns
Tuple patterns are useful when the decision depends on multiple values.
public static decimal CalculateShipping(decimal orderTotal, bool isExpress, bool isInternational)
{
return (orderTotal, isExpress, isInternational) switch
{
(>= 1000, false, false) => 0m,
(_, true, false) => 25m,
(_, false, true) => 40m,
(_, true, true) => 60m,
_ => 10m
};
}
Tuple patterns are useful for:
- Combining multiple flags
- Mapping pairs of values
- State transition rules
- Small decision tables
- Coordinate-style logic
However, tuple-heavy switch expressions can become difficult to read when there are too many values. For complex rules, consider a named type, a rule object, a strategy pattern, or a decision table.
List Patterns
List patterns match sequences such as arrays or lists.
public static string DescribeNumbers(int[] numbers)
{
return numbers switch
{
[] => "Empty",
[var single] => $"Single value: {single}",
[1, 2, 3] => "One two three",
[var first, .., var last] => $"Starts with {first}, ends with {last}"
};
}
List patterns are useful for:
- Command parsing
- Token matching
- Small array classification
- Detecting sequence shape
- Matching first and last elements
Example:
public static string ParseCommand(string[] args)
{
return args switch
{
["create", var name] => $"Create {name}",
["delete", var id] => $"Delete {id}",
["list"] => "List all",
_ => "Unknown command"
};
}
List patterns are powerful, but they should be used carefully. If matching sequence shape becomes too complex, a parser or clearer validation logic may be better.
Case Guards with when
A case guard adds an additional condition to a switch arm.
public static string GetPaymentMessage(decimal amount, bool isVip)
{
return amount switch
{
<= 0 => "Invalid amount",
>= 1000 when isVip => "VIP high-value payment",
>= 1000 => "High-value payment",
_ => "Standard payment"
};
}
The pattern must match first, and then the when condition must evaluate to true.
Case guards are useful when:
- A pattern handles the shape, and a condition handles extra business logic
- The condition depends on external variables
- A simple pattern cannot express the full rule clearly
Avoid using too many when clauses in one switch expression. It may become harder to read than if/else.
Null Handling
Switch expressions work well with null checks.
public static string GetDisplayName(User? user)
{
return user switch
{
null => "Anonymous",
{ FirstName: not null and not "", LastName: not null and not "" } =>
$"{user.FirstName} {user.LastName}",
{ FirstName: not null and not "" } => user.FirstName,
_ => "Unnamed User"
};
}
public sealed class User
{
public string? FirstName { get; init; }
public string? LastName { get; init; }
}
A simpler and safer version can capture property values:
public static string GetDisplayName(User? user)
{
return user switch
{
null => "Anonymous",
{ FirstName: { Length: > 0 } firstName, LastName: { Length: > 0 } lastName } =>
$"{firstName} {lastName}",
{ FirstName: { Length: > 0 } firstName } => firstName,
_ => "Unnamed User"
};
}
Important null-safety habit:
- Handle
nullexplicitly when input may be null - Avoid using the null-forgiving operator
!to silence warnings unless you have a strong reason - Use property patterns to check nested values safely
- Keep null handling near the decision logic
Result Type and Type Inference
A switch expression must produce a result. The compiler needs to determine a common type for all arms.
Valid example:
var value = status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Paid => "Paid",
_ => "Unknown"
};
All arms return string.
Invalid example:
var value = status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Paid => 1,
_ => false
};
The arms return unrelated types, so the compiler cannot infer a useful common type.
A target type can help:
object value = status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Paid => 1,
_ => false
};
This compiles because all values can be assigned to object, but it may be a poor design if the caller expects a meaningful consistent type.
Best practice:
- Keep all arms returning the same conceptual type
- Avoid using
objectjust to make mixed result types compile - Prefer domain-specific result types when the output has meaning
Throw Expressions in Switch Arms
A switch arm can throw an exception.
public static string GetRequiredLabel(OrderStatus status)
{
return status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Submitted => "Submitted",
OrderStatus.Paid => "Paid",
OrderStatus.Cancelled => "Cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unsupported status.")
};
}
This is useful when unexpected input should fail fast.
Common examples:
- Unsupported enum value
- Invalid state transition
- Unknown command
- Unexpected external system code
- Missing required mapping
Do not throw for normal business outcomes. If the result is expected, return a meaningful value instead.
Real-World Usage
Switch expressions are common in production C# code for mapping and classification.
Mapping Domain Status to API Response
public static string ToApiStatus(OrderStatus status)
{
return status switch
{
OrderStatus.Draft => "draft",
OrderStatus.Submitted => "submitted",
OrderStatus.Paid => "paid",
OrderStatus.Cancelled => "cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unsupported order status.")
};
}
Mapping HTTP Status Codes
public static string GetHttpCategory(int statusCode)
{
return statusCode switch
{
>= 100 and <= 199 => "Informational",
>= 200 and <= 299 => "Success",
>= 300 and <= 399 => "Redirection",
>= 400 and <= 499 => "Client Error",
>= 500 and <= 599 => "Server Error",
_ => "Unknown"
};
}
State Transition Validation
public static bool CanTransition(OrderStatus current, OrderStatus next)
{
return (current, next) switch
{
(OrderStatus.Draft, OrderStatus.Submitted) => true,
(OrderStatus.Submitted, OrderStatus.Paid) => true,
(OrderStatus.Draft, OrderStatus.Cancelled) => true,
(OrderStatus.Submitted, OrderStatus.Cancelled) => true,
_ => false
};
}
DTO Mapping
public sealed class OrderDto
{
public required string Status { get; init; }
public required string Label { get; init; }
}
public static OrderDto ToDto(Order order)
{
return new OrderDto
{
Status = order.Status switch
{
OrderStatus.Draft => "draft",
OrderStatus.Submitted => "submitted",
OrderStatus.Paid => "paid",
OrderStatus.Cancelled => "cancelled",
_ => "unknown"
},
Label = order.Status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Submitted => "Submitted",
OrderStatus.Paid => "Paid",
OrderStatus.Cancelled => "Cancelled",
_ => "Unknown"
}
};
}
public sealed class Order
{
public OrderStatus Status { get; init; }
}
If the same mapping is repeated in many places, extract it into a method or a mapper to avoid duplication.
Trade-Offs
Switch expressions are concise, but they are not always the best tool.
Advantages:
- Clear for value mapping
- Removes repetitive
caseandbreak - Works naturally with expression-bodied members
- Supports powerful pattern matching
- Encourages branch logic to return a value
- Reduces some common switch statement mistakes
Disadvantages:
- Can become unreadable when too many patterns are combined
- Can hide complex business rules in a compact expression
- Can encourage type checking instead of polymorphism
- Can become hard to debug if each arm contains complex expressions
- Requires careful arm ordering
- Requires careful handling of non-exhaustive cases
Good usage:
public static string GetLabel(OrderStatus status) => status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Submitted => "Submitted",
OrderStatus.Paid => "Paid",
OrderStatus.Cancelled => "Cancelled",
_ => "Unknown"
};
Poor usage:
public static decimal Calculate(Order order, Customer customer, DateTime now)
{
return (order.Status, customer.Type, order.TotalAmount, now.DayOfWeek) switch
{
(OrderStatus.Paid, "VIP", >= 1000, DayOfWeek.Monday) when customer.HasCoupon =>
ApplyMultipleRules(order, customer, now),
(OrderStatus.Submitted, "Partner", >= 500, _) when IsSpecialCampaign(now) =>
CalculatePartnerPromotion(order, customer, now),
_ => CalculateDefault(order)
};
}
This may be better as named business rules, a strategy pattern, or a dedicated pricing service.
Common Mistakes
Mistake 1: Forgetting the Catch-All Arm
var label = status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Paid => "Paid"
};
This is risky because not all enum values are handled.
Better:
var label = status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Submitted => "Submitted",
OrderStatus.Paid => "Paid",
OrderStatus.Cancelled => "Cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unsupported status.")
};
Mistake 2: Putting the General Arm Before Specific Arms
var category = amount switch
{
> 0 => "Positive",
> 1000 => "Large",
_ => "Other"
};
The > 1000 arm is unreachable in practice because > 0 catches it first.
Better:
var category = amount switch
{
> 1000 => "Large",
> 0 => "Positive",
_ => "Other"
};
Mistake 3: Using Switch Expressions for Side Effects
Avoid this:
_ = status switch
{
OrderStatus.Paid => SendReceipt(),
OrderStatus.Cancelled => SendCancellationEmail(),
_ => Task.CompletedTask
};
This is less clear than straightforward imperative logic.
Better:
switch (status)
{
case OrderStatus.Paid:
await SendReceipt();
break;
case OrderStatus.Cancelled:
await SendCancellationEmail();
break;
}
Mistake 4: Overusing _
A catch-all arm can hide missing cases.
public static string GetLabel(OrderStatus status)
{
return status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Paid => "Paid",
_ => "Unknown"
};
}
If a new enum value is added, this method silently returns "Unknown".
For internal domain logic, throwing may be safer:
public static string GetLabel(OrderStatus status)
{
return status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Submitted => "Submitted",
OrderStatus.Paid => "Paid",
OrderStatus.Cancelled => "Cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unsupported order status.")
};
}
Mistake 5: Making Patterns Too Clever
This may be compact but hard to maintain:
return (customer.Type, order.TotalAmount, order.CreatedAt.DayOfWeek, order.Items.Count) switch
{
("VIP" or "Partner", >= 1000, not DayOfWeek.Sunday, > 5) => 0.25m,
("VIP", >= 500, _, _) => 0.15m,
(_, >= 2000, _, > 10) => 0.10m,
_ => 0m
};
A clearer version may use named helper methods:
if (IsLargeVipOrPartnerOrder(customer, order))
{
return 0.25m;
}
if (IsVipOrder(customer, order))
{
return 0.15m;
}
if (IsBulkHighValueOrder(order))
{
return 0.10m;
}
return 0m;
Switch expressions are not a replacement for readable design.
Best Practices
Use switch expressions when:
- Each branch returns a value
- The mapping is short and clear
- Patterns describe the business rule naturally
- The logic is easier to read than
if/else - The result type is consistent
- There are no significant side effects
Avoid or reconsider switch expressions when:
- Each branch performs multiple actions
- Branches require long blocks of code
- The decision depends on many unrelated values
- Business rules are complex and change often
- The code is type switching where polymorphism would be better
- The
_arm hides important missing cases
Recommended habits:
- Handle
nullexplicitly when needed - Put specific cases before general cases
- Keep result expressions short
- Extract repeated mappings into methods
- Throw for impossible states in internal domain logic
- Return fallback values for expected external or user input cases
- Prefer property patterns over tuple patterns when names improve readability
- Use tests to cover important switch arms
- Revisit switch expressions when new enum values or domain states are added
Switch Expressions vs Polymorphism
Switch expressions are good for simple mappings. Polymorphism is better when behavior varies by type and each type owns its behavior.
Switch expression approach:
public static decimal CalculateFee(PaymentMethod paymentMethod)
{
return paymentMethod switch
{
CreditCardPayment => 2.50m,
BankTransferPayment => 1.00m,
CashPayment => 0.00m,
_ => throw new ArgumentOutOfRangeException(nameof(paymentMethod))
};
}
public abstract class PaymentMethod;
public sealed class CreditCardPayment : PaymentMethod;
public sealed class BankTransferPayment : PaymentMethod;
public sealed class CashPayment : PaymentMethod;
Polymorphic approach:
public abstract class PaymentMethod
{
public abstract decimal CalculateFee();
}
public sealed class CreditCardPayment : PaymentMethod
{
public override decimal CalculateFee() => 2.50m;
}
public sealed class BankTransferPayment : PaymentMethod
{
public override decimal CalculateFee() => 1.00m;
}
public sealed class CashPayment : PaymentMethod
{
public override decimal CalculateFee() => 0.00m;
}
Use switch expressions when the operation is external mapping or classification. Use polymorphism when the behavior belongs naturally to the type and is expected to grow.
Switch Expressions vs Dictionary Lookup
A dictionary can be better for simple static mappings.
Switch expression:
public static string GetCurrencySymbol(string currencyCode)
{
return currencyCode switch
{
"USD" => "$",
"EUR" => "€",
"GBP" => "£",
"JPY" => "¥",
_ => ""
};
}
Dictionary approach:
private static readonly Dictionary<string, string> CurrencySymbols = new()
{
["USD"] = "$",
["EUR"] = "€",
["GBP"] = "£",
["JPY"] = "¥"
};
public static string GetCurrencySymbol(string currencyCode)
{
return CurrencySymbols.TryGetValue(currencyCode, out var symbol)
? symbol
: "";
}
Use a switch expression when the mapping is small, strongly typed, and unlikely to change. Use a dictionary when mappings are larger, data-driven, configurable, or frequently updated.
Switch Expressions vs if/else
Use if/else when conditions are independent, procedural, or require multiple statements.
Good switch expression candidate:
var label = status switch
{
OrderStatus.Draft => "Draft",
OrderStatus.Paid => "Paid",
_ => "Unknown"
};
Good if/else candidate:
if (order is null)
{
logger.LogWarning("Order was null.");
return;
}
if (!order.IsPaid)
{
await paymentService.RequestPaymentAsync(order);
return;
}
await shippingService.ScheduleShipmentAsync(order);
A practical rule: if the code is primarily choosing a value, consider a switch expression. If the code is performing a workflow, use if/else, a switch statement, or a dedicated service.