DEV_NET_CORE
GET_STARTED
.NETModern C# patterns

Switch Expressions in C#

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:

Code
var result = input switch
{
    pattern1 => value1,
    pattern2 => value2,
    _ => defaultValue
};

A switch expression contains:

  • An input expression before the switch keyword
  • One or more switch arms
  • A pattern for each arm
  • An optional when guard
  • The => token
  • A result expression
  • Commas between arms
  • A semicolon after the full expression when used as a statement

Example:

Code
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:

Code
string label;

switch (status)
{
    case OrderStatus.Draft:
        label = "Draft";
        break;

    case OrderStatus.Paid:
        label = "Paid";
        break;

    default:
        label = "Unknown";
        break;
}

Switch expression equivalent:

Code
string label = status switch
{
    OrderStatus.Draft => "Draft",
    OrderStatus.Paid => "Paid",
    _ => "Unknown"
};

Important differences:

FeatureSwitch StatementSwitch Expression
Main purposeExecute statementsReturn a value
Syntaxcase, break, defaultpatterns, =>, _
Best forMultiple side effects or complex blocksMapping, classification, value selection
Fall-throughControlled with break, goto, etc.No fall-through
ExhaustivenessLess expression-focusedMore important because a value must be produced
ReadabilityBetter for multi-step imperative logicBetter for concise result-producing logic

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.

Code
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:

Code
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.

Code
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:

Code
public static string GetPriorityLabel(int priority)
{
    return priority switch
    {
        1 => "Low",
        2 => "Medium",
        3 => "High"
    };
}

If priority is 4, no arm matches.

Safer version:

Code
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:

Code
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:

Code
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.

Code
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:

Code
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 >=.

Code
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:

Code
public static string ClassifyScore(int score)
{
    return score switch
    {
        >= 50 => "Pass",
        >= 90 => "Excellent",
        _ => "Fail"
    };
}

Correct version:

Code
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.

Code
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:

Code
public static bool IsWeekend(DayOfWeek day)
{
    return day switch
    {
        DayOfWeek.Saturday or DayOfWeek.Sunday => true,
        _ => false
    };
}

The not pattern can express exclusions:

Code
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:

Code
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.

Code
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:

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.

Code
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:

Code
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.

Code
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:

Code
// 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:

Code
// 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.

Code
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.

Code
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:

Code
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.

Code
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.

Code
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:

Code
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 null explicitly 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:

Code
var value = status switch
{
    OrderStatus.Draft => "Draft",
    OrderStatus.Paid => "Paid",
    _ => "Unknown"
};

All arms return string.

Invalid example:

Code
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:

Code
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 object just 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.

Code
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

Code
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

Code
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

Code
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

Code
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 case and break
  • 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:

Code
public static string GetLabel(OrderStatus status) => status switch
{
    OrderStatus.Draft => "Draft",
    OrderStatus.Submitted => "Submitted",
    OrderStatus.Paid => "Paid",
    OrderStatus.Cancelled => "Cancelled",
    _ => "Unknown"
};

Poor usage:

Code
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

Code
var label = status switch
{
    OrderStatus.Draft => "Draft",
    OrderStatus.Paid => "Paid"
};

This is risky because not all enum values are handled.

Better:

Code
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

Code
var category = amount switch
{
    > 0 => "Positive",
    > 1000 => "Large",
    _ => "Other"
};

The > 1000 arm is unreachable in practice because > 0 catches it first.

Better:

Code
var category = amount switch
{
    > 1000 => "Large",
    > 0 => "Positive",
    _ => "Other"
};

Mistake 3: Using Switch Expressions for Side Effects

Avoid this:

Code
_ = status switch
{
    OrderStatus.Paid => SendReceipt(),
    OrderStatus.Cancelled => SendCancellationEmail(),
    _ => Task.CompletedTask
};

This is less clear than straightforward imperative logic.

Better:

Code
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.

Code
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:

Code
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:

Code
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:

Code
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 null explicitly 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:

Code
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:

Code
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:

Code
public static string GetCurrencySymbol(string currencyCode)
{
    return currencyCode switch
    {
        "USD" => "$",
        "EUR" => "€",
        "GBP" => "£",
        "JPY" => "¥",
        _ => ""
    };
}

Dictionary approach:

Code
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:

Code
var label = status switch
{
    OrderStatus.Draft => "Draft",
    OrderStatus.Paid => "Paid",
    _ => "Unknown"
};

Good if/else candidate:

Code
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.

Interview Practice

PreviousRecords in C#Next UpAsync and Await Semantics