Learnixo

.NET & C# Development · Lesson 49 of 229

State — Behavior Changes With Internal State

State — Behaviour Changes With Internal State

The State pattern allows an object to change its behaviour when its internal state changes. Instead of a giant switch/if-else on state, each state is a class with its own behaviour. The object delegates to its current state object.


The Problem

C#
// Without State: complex switch/if — hard to add new states
public class Order
{
    public string Status { get; set; } = "Draft";

    public void Submit()
    {
        switch (Status)
        {
            case "Draft":    Status = "Submitted"; break;
            case "Submitted": throw new InvalidOperationException("Already submitted");
            case "Shipped":  throw new InvalidOperationException("Cannot resubmit shipped order");
            // ... growing every time we add a state
        }
    }
}

State Pattern Implementation

C#
// State interface
public interface IOrderState
{
    void Submit(Order order);
    void Pay(Order order);
    void Ship(Order order);
    void Cancel(Order order);
    string StatusName { get; }
}

// Context — delegates all behaviour to current state
public class Order
{
    private IOrderState _state = new DraftState();

    public int     Id       { get; init; }
    public decimal Total    { get; init; }
    public string  Status   => _state.StatusName;

    internal void TransitionTo(IOrderState state)
    {
        Console.WriteLine($"Order {Id}: {_state.StatusName} → {state.StatusName}");
        _state = state;
    }

    public void Submit()  => _state.Submit(this);
    public void Pay()     => _state.Pay(this);
    public void Ship()    => _state.Ship(this);
    public void Cancel()  => _state.Cancel(this);
}

// Concrete States
public class DraftState : IOrderState
{
    public string StatusName => "Draft";

    public void Submit(Order order) => order.TransitionTo(new SubmittedState());
    public void Pay(Order order)    => throw new InvalidOperationException("Submit before paying");
    public void Ship(Order order)   => throw new InvalidOperationException("Not yet paid");
    public void Cancel(Order order) => order.TransitionTo(new CancelledState());
}

public class SubmittedState : IOrderState
{
    public string StatusName => "Submitted";

    public void Submit(Order order) => throw new InvalidOperationException("Already submitted");
    public void Pay(Order order)    => order.TransitionTo(new PaidState());
    public void Ship(Order order)   => throw new InvalidOperationException("Payment required first");
    public void Cancel(Order order) => order.TransitionTo(new CancelledState());
}

public class PaidState : IOrderState
{
    public string StatusName => "Paid";

    public void Submit(Order order) => throw new InvalidOperationException("Already submitted");
    public void Pay(Order order)    => throw new InvalidOperationException("Already paid");
    public void Ship(Order order)   => order.TransitionTo(new ShippedState());
    public void Cancel(Order order) => order.TransitionTo(new RefundingState());
}

public class ShippedState : IOrderState
{
    public string StatusName => "Shipped";

    public void Submit(Order order) => throw new InvalidOperationException("Order shipped");
    public void Pay(Order order)    => throw new InvalidOperationException("Order shipped");
    public void Ship(Order order)   => throw new InvalidOperationException("Already shipped");
    public void Cancel(Order order) => throw new InvalidOperationException("Cannot cancel shipped order");
}

public class CancelledState : IOrderState
{
    public string StatusName => "Cancelled";
    public void Submit(Order order) => throw new InvalidOperationException("Order cancelled");
    public void Pay(Order order)    => throw new InvalidOperationException("Order cancelled");
    public void Ship(Order order)   => throw new InvalidOperationException("Order cancelled");
    public void Cancel(Order order) => throw new InvalidOperationException("Already cancelled");
}

public class RefundingState : IOrderState
{
    public string StatusName => "Refunding";
    public void Submit(Order order) => throw new InvalidOperationException("Order being refunded");
    public void Pay(Order order)    => throw new InvalidOperationException("Order being refunded");
    public void Ship(Order order)   => throw new InvalidOperationException("Order being refunded");
    public void Cancel(Order order) => throw new InvalidOperationException("Already cancelled");
}

// Usage
var order = new Order { Id = 1, Total = 99.99m };
order.Submit();   // Draft → Submitted
order.Pay();      // Submitted → Paid
order.Ship();     // Paid → Shipped

Lightweight State with Enum + Record

C#
// For simple state machines without complex per-state behaviour
public enum TrafficLight { Red, Yellow, Green }

public class TrafficLightController
{
    private TrafficLight _current = TrafficLight.Red;

    public void Advance()
    {
        _current = _current switch
        {
            TrafficLight.Red    => TrafficLight.Green,
            TrafficLight.Green  => TrafficLight.Yellow,
            TrafficLight.Yellow => TrafficLight.Red,
            _ => throw new InvalidOperationException("Unknown state"),
        };
    }

    public string Current => _current.ToString();
}

Interview Answer

"The State pattern encodes each state as a class that implements a common interface. The context object delegates all state-dependent behaviour to its current state object — no switch statement needed. Adding a new state means adding a new class; existing states don't change (open/closed principle). The pattern shines for domain objects with complex lifecycles: orders (Draft → Submitted → Paid → Shipped), workflows (Pending → InReview → Approved → Published), and connection objects (Disconnected → Connecting → Connected → Failed). The trade-off: simple state machines with 2–3 states and limited transitions are cleaner with an enum + switch expression — the full State pattern is justified when behaviour per state is complex or states need to grow independently."