.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
// 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
// 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 → ShippedLightweight State with Enum + Record
// 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."