BrandGhost
C# Enum Switch: Pattern Matching and Exhaustive Checks

C# Enum Switch: Pattern Matching and Exhaustive Checks

C# Enum Switch: Pattern Matching and Exhaustive Checks

When a variable can only hold one of a known set of values, switch is the natural control flow. With C# enum switch expressions you get more than branching -- you get type-checked arms, tuple decomposition, guard clauses, and the ability to express business rules as data instead of nested conditionals.

This guide covers the full range of switch patterns with enums: classic switch statement, switch expression, exhaustive checking, multi-case arms, tuple patterns, guard clauses, and how to use analyzers to catch missing cases at compile time.

Classic Switch Statement With Enum

The switch statement remains valid and is well-understood by every C# developer:

public enum OrderStatus
{
    Pending,
    Processing,
    Shipped,
    Delivered,
    Cancelled
}

void HandleStatus(OrderStatus status)
{
    switch (status)
    {
        case OrderStatus.Pending:
            StartProcessing();
            break;
        case OrderStatus.Shipped:
        case OrderStatus.Delivered:
            NotifyCustomer();
            break;
        case OrderStatus.Cancelled:
            IssueRefund();
            break;
        case OrderStatus.Processing:
            // no action needed
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(status), status, null);
    }
}

Key points:

  • case OrderStatus.Shipped: falls through to case OrderStatus.Delivered: -- classic multi-case grouping
  • The default arm throws on unknown values, protecting against future enum members being silently ignored
  • Side effects (method calls) fit naturally in a statement-based switch

Switch Expression With Enum

The switch expression (C# 8+) is a concise expression-level alternative:

string Describe(OrderStatus status) => status switch
{
    OrderStatus.Pending    => "Waiting to be processed",
    OrderStatus.Processing => "Currently being handled",
    OrderStatus.Shipped    => "On the way",
    OrderStatus.Delivered  => "Delivered",
    OrderStatus.Cancelled  => "Cancelled",
    _                      => throw new ArgumentOutOfRangeException(nameof(status))
};

The switch expression:

  • Returns a value (useful for mapping enums to strings, integers, other enums, or objects)
  • Has no break -- each arm is a => expression
  • Is an expression itself, so it composes inside method return values, variable assignments, and lambdas
  • Produces a compiler warning if not all enum members are covered and no discard arm is present

The discard arm _ is the catch-all. Whether to use it depends on whether you want runtime detection of unhandled cases (throw) or a safe fallback (=> defaultValue).

Exhaustive Checking: Compiler vs Runtime

The C# compiler issues CS8509 when a switch expression does not cover all known enum members and has no discard arm. Note that the warning requires the input to be statically typed as the specific enum -- it will not fire if you switch on object, Enum, or a dynamically typed value:

// CS8509: The switch expression does not handle all possible values of its input type
// (it is not exhaustive). For example, the pattern 'OrderStatus.Cancelled' is not covered.
string Describe(OrderStatus status) => status switch
{
    OrderStatus.Pending    => "Pending",
    OrderStatus.Processing => "Processing",
    OrderStatus.Shipped    => "Shipped",
    OrderStatus.Delivered  => "Delivered"
    // Missing: Cancelled
};

The warning fires only for switch expressions, not switch statements. If you add a discard arm, the warning is suppressed -- but you lose compile-time detection of future additions.

Two strategies:

Strategy 1: Use a discard arm that throws. New members are caught at runtime, immediately and loudly:

string Describe(OrderStatus status) => status switch
{
    OrderStatus.Pending    => "Pending",
    OrderStatus.Processing => "Processing",
    OrderStatus.Shipped    => "Shipped",
    OrderStatus.Delivered  => "Delivered",
    OrderStatus.Cancelled  => "Cancelled",
    _                      => throw new ArgumentOutOfRangeException(
                                  nameof(status), $"Unhandled: {status}")
};

Strategy 2: No discard arm. The CS8509 warning fires at compile time when you add a new member without updating the switch. This is the safer option when the switch must be exhaustive by design:

// Enable warning-as-error in .csproj for this case:
// <TreatWarningsAsErrors>CS8509</TreatWarningsAsErrors>

In a team codebase, treating CS8509 as an error ensures that every switch expression stays up to date as the enum grows.

Multi-Case Arms and When Guards

The switch expression supports multiple patterns per arm using or:

string GetCategory(OrderStatus status) => status switch
{
    OrderStatus.Pending or
    OrderStatus.Processing   => "Active",

    OrderStatus.Shipped or
    OrderStatus.Delivered    => "Fulfilled",

    OrderStatus.Cancelled    => "Inactive",

    _ => throw new ArgumentOutOfRangeException(nameof(status))
};

or in a switch arm is different from the bitwise | -- it is a pattern combinator.

Guard clauses (the when keyword) add conditional logic to a switch arm:

public record Order(OrderStatus Status, DateTimeOffset? ShippedAt);

string GetMessage(Order order) => order.Status switch
{
    OrderStatus.Shipped when order.ShippedAt.HasValue =>
        $"Shipped on {order.ShippedAt.Value:D}",

    OrderStatus.Shipped =>
        "Shipped (date unavailable)",

    OrderStatus.Delivered =>
        "Delivered",

    _ => "Other"
};

When guards allow you to branch on additional context without nesting if statements inside switch arms.

Tuple Pattern Matching With Enum

Combining two enum values (or an enum and another value) into a tuple creates a powerful branching mechanism:

public enum OrderEvent
{
    StartProcessing,
    Ship,
    Deliver,
    Cancel
}

OrderStatus Transition(OrderStatus current, OrderEvent @event) =>
    (current, @event) switch
    {
        (OrderStatus.Pending,    OrderEvent.StartProcessing) => OrderStatus.Processing,
        (OrderStatus.Processing, OrderEvent.Ship)            => OrderStatus.Shipped,
        (OrderStatus.Shipped,    OrderEvent.Deliver)         => OrderStatus.Delivered,
        (OrderStatus.Pending,    OrderEvent.Cancel)          => OrderStatus.Cancelled,
        (OrderStatus.Processing, OrderEvent.Cancel)          => OrderStatus.Cancelled,
        _ => throw new InvalidOperationException(
                $"Transition {@event} is not valid from state {current}")
    };

This is the state machine pattern: every valid transition is expressed as one row in the switch. Invalid transitions throw immediately. The structure is flat and easy to read, test, and extend. For a deeper look at how this integrates with the State Design Pattern, see State Pattern Best Practices in C#.

Enum Switch in Factory and Strategy Patterns

Switch expressions with enums are commonly used in factory and strategy scenarios:

public interface INotificationChannel
{
    Task SendAsync(string message);
}

public enum NotificationType { Email, Sms, Push }

INotificationChannel CreateChannel(
    NotificationType type,
    IServiceProvider services) =>
    type switch
    {
        NotificationType.Email => services.GetRequiredService<IEmailChannel>(),
        NotificationType.Sms   => services.GetRequiredService<ISmsChannel>(),
        NotificationType.Push  => services.GetRequiredService<IPushChannel>(),
        _                      => throw new ArgumentOutOfRangeException(
                                      nameof(type), $"Unknown: {type}")
    };

This is a functional factory: given a type enum, return the appropriate service. It is easy to extend -- add a new member to NotificationType, add an arm to the switch, add a service registration. The compiler warns if you miss the switch arm (when no _ is present).

For the Command Design Pattern, enums often identify which command to dispatch:

public enum Command { Start, Stop, Pause, Resume }

void Execute(Command command) =>
    _ = command switch
    {
        Command.Start  => StartService(),
        Command.Stop   => StopService(),
        Command.Pause  => PauseService(),
        Command.Resume => ResumeService(),
        _              => throw new ArgumentOutOfRangeException(nameof(command))
    };

// Helper to use switch expression as statement (C# 8+)
static T Identity<T>(T value) => value;

Note: when using a switch expression purely for side effects (the arms call void methods), you need a _ = discard assignment or wrap the calls in expressions that return a value.

Practical: Returning Different Types

Switch expressions can return any type, including complex objects:

// Return different DTO types based on enum
public record PendingOrderView(Guid Id, string CustomerName);
public record ShippedOrderView(Guid Id, string TrackingNumber);

object GetOrderView(Order order) => order.Status switch
{
    OrderStatus.Pending  => new PendingOrderView(order.Id, order.CustomerName),
    OrderStatus.Shipped  => new ShippedOrderView(order.Id, order.TrackingNumber),
    OrderStatus.Delivered => new ShippedOrderView(order.Id, order.TrackingNumber),
    _                    => order
};

When the return type must be a specific base type or interface, the common type is inferred or you specify it explicitly.

Analyzing Switch Coverage With Roslyn

The built-in CS8509 warning is the first line of defense. For more rigorous enforcement:

Treat CS8509 as an error in your project file:

<!-- .csproj -->
<PropertyGroup>
    <Nullable>enable</Nullable>
    <WarningsAsErrors>CS8509</WarningsAsErrors>
</PropertyGroup>

This makes the build fail if any switch expression is non-exhaustive. Any new enum member that is not handled in all switch expressions produces a compile error.

Use IDE0072 (ReSharper / Rider): "Add missing switch cases" -- surfaces the same exhaustiveness issue in the IDE.

Common Mistakes With Enum Switch

Using the default arm in a switch expression with a throw, but also wanting compile-time completeness. These are mutually exclusive. Choose one: compile-time completeness (no discard arm, treat CS8509 as error) or runtime completeness (discard arm with throw).

Not throwing in the default arm. A default: break in a switch statement silently ignores unknown values. Always throw or log for unrecognized values.

Ignoring new enum members during code review. When an enum gains a new member, search for all switches on that enum type. Tools help, but the discipline of doing it is required.

Using a switch to dispatch to many different behaviors when a dictionary or registry pattern would be cleaner. When behavior becomes dynamic, varies per environment, or needs to be extensible without recompiling, a dictionary of delegates or a registry class is easier to extend and test.

For a complete foundational picture of how C# enum values work before applying switch patterns, see C# Enum: Complete Guide to Enumerations and How to Use Enum in C#. For the classic switch statement syntax, Beginner's CSharp Switch Statement Tutorial is a good companion reference.

Frequently Asked Questions

How do I use a switch expression with a C# enum?

Write myEnum switch { EnumType.Member => value, ... }. Each arm is pattern => expression. Add a _ discard arm for the catch-all. The switch expression returns a value, making it composable and concise.

How do I make a C# switch expression exhaustive?

Remove the discard arm and treat compiler warning CS8509 as an error in your .csproj. This forces every switch expression on that enum to handle all members or produce a compile error.

What is the difference between a switch statement and a switch expression in C#?

A switch statement executes statements and supports side effects, fallthrough (via empty cases), and break. A switch expression returns a value, requires no break, supports no fallthrough, and issues compiler warnings for non-exhaustive coverage. Prefer the switch expression for mapping and prefer the switch statement for complex side-effectful logic.

How do I handle multiple enum values in one switch arm?

In a switch expression, use or to combine patterns: OrderStatus.Shipped or OrderStatus.Delivered => "Fulfilled". In a classic switch statement, stack empty cases before the handled case to achieve the same grouping.

Can I use guard clauses in a C# enum switch?

Yes. Use when after the pattern: OrderStatus.Shipped when order.HasTrackingNumber => "Track your order". The when clause adds a boolean condition that must be true for the arm to match.

What is the CS8509 warning in C#?

CS8509 fires when a switch expression is not exhaustive -- it does not cover all members of the enum type and has no discard arm. Add the missing arms or a _ arm to suppress it. For strict codebases, treat it as a compile error.

How do I use enum switch for a state machine in C#?

Use a tuple pattern with (currentState, event) switch { (State.A, Event.X) => State.B, ... }. Each row maps a valid transition. Unrecognized combinations throw an InvalidOperationException. This pattern is the simplest form of a state machine in C#.

The CSharp Switch Statement - How To Go From Zero To Hero

Discover how to master the CSharp switch statement to make your code more effective and efficient. Follow along with C# switch statement examples!

Beginner's CSharp Switch Statement Tutorial: How To Properly Use A Switch Statement

In this CSharp switch statement tutorial, you'll learn how to properly use switch statements in your programs. Explore examples, features, and best practices!

C# Enum: Complete Guide to Enumerations in .NET

Master C# enum with this complete guide. Learn to declare enums, use flags, convert to string, apply switch pattern matching, and follow .NET best practices.

An error has occurred. This application may no longer respond until reloaded. Reload