BehavioralC#verifiedVerified
State Pattern in C#
Allows an object to alter its behavior when its internal state changes, appearing as if the object has changed its class.
How to Implement the State Pattern in C#
1Step 1: Define the state interface
public interface IState
{
void Handle(Context context);
string Name { get; }
}2Step 2: Context delegates behavior to the current state
public class Context
{
public IState CurrentState { get; private set; }
public Context(IState initial) => CurrentState = initial;
public void TransitionTo(IState state)
{
Console.WriteLine($"Transition: {CurrentState.Name} -> {state.Name}");
CurrentState = state;
}
public void Request() => CurrentState.Handle(this);
}3Step 3: Concrete states
public class IdleState : IState
{
public string Name => "Idle";
public void Handle(Context context)
{
Console.WriteLine("Idle: Starting work...");
context.TransitionTo(new ActiveState());
}
}
public class ActiveState : IState
{
public string Name => "Active";
public void Handle(Context context)
{
Console.WriteLine("Active: Finishing work...");
context.TransitionTo(new IdleState());
}
}
// Usage:
// var ctx = new Context(new IdleState());
// ctx.Request(); // Idle -> Active
// ctx.Request(); // Active -> Idleusing Microsoft.Extensions.Logging;
// [step] Define order state machine with strongly-typed transitions
public enum OrderStatus
{
Draft, Submitted, Approved, Shipped, Delivered, Cancelled
}
public record OrderEvent(string Name, DateTime Timestamp, string? Reason = null);
public interface IOrderState
{
OrderStatus Status { get; }
Task<IOrderState> SubmitAsync(Order order, CancellationToken ct = default);
Task<IOrderState> ApproveAsync(Order order, CancellationToken ct = default);
Task<IOrderState> ShipAsync(Order order, CancellationToken ct = default);
Task<IOrderState> DeliverAsync(Order order, CancellationToken ct = default);
Task<IOrderState> CancelAsync(Order order, string reason,
CancellationToken ct = default);
}
// [step] Order context with audit trail
public sealed class Order(
string id, IOrderState initialState, ILogger<Order> logger)
{
private IOrderState _state = initialState;
private readonly List<OrderEvent> _events = [];
public string Id => id;
public OrderStatus Status => _state.Status;
public IReadOnlyList<OrderEvent> Events => _events.AsReadOnly();
private async Task<IOrderState> TransitionAsync(
Func<Task<IOrderState>> action, string eventName,
CancellationToken ct)
{
var previousStatus = _state.Status;
_state = await action();
_events.Add(new OrderEvent(eventName, DateTime.UtcNow));
logger.LogInformation("Order {Id}: {From} -> {To}",
id, previousStatus, _state.Status);
return _state;
}
public Task SubmitAsync(CancellationToken ct = default) =>
TransitionAsync(() => _state.SubmitAsync(this, ct), "Submit", ct);
public Task ApproveAsync(CancellationToken ct = default) =>
TransitionAsync(() => _state.ApproveAsync(this, ct), "Approve", ct);
public Task ShipAsync(CancellationToken ct = default) =>
TransitionAsync(() => _state.ShipAsync(this, ct), "Ship", ct);
public Task DeliverAsync(CancellationToken ct = default) =>
TransitionAsync(() => _state.DeliverAsync(this, ct), "Deliver", ct);
public Task CancelAsync(string reason, CancellationToken ct = default) =>
TransitionAsync(
() => _state.CancelAsync(this, reason, ct), "Cancel", ct);
}
// [step] Concrete states with valid transition enforcement
public class DraftState : IOrderState
{
public OrderStatus Status => OrderStatus.Draft;
public Task<IOrderState> SubmitAsync(Order order, CancellationToken ct) =>
Task.FromResult<IOrderState>(new SubmittedState());
public Task<IOrderState> CancelAsync(
Order order, string reason, CancellationToken ct) =>
Task.FromResult<IOrderState>(new CancelledState());
public Task<IOrderState> ApproveAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Cannot approve a draft order");
public Task<IOrderState> ShipAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Cannot ship a draft order");
public Task<IOrderState> DeliverAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Cannot deliver a draft order");
}
public class SubmittedState : IOrderState
{
public OrderStatus Status => OrderStatus.Submitted;
public Task<IOrderState> ApproveAsync(Order order, CancellationToken ct) =>
Task.FromResult<IOrderState>(new ApprovedState());
public Task<IOrderState> CancelAsync(
Order order, string reason, CancellationToken ct) =>
Task.FromResult<IOrderState>(new CancelledState());
public Task<IOrderState> SubmitAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Already submitted");
public Task<IOrderState> ShipAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Must approve before shipping");
public Task<IOrderState> DeliverAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Must ship before delivering");
}
public class ApprovedState : IOrderState
{
public OrderStatus Status => OrderStatus.Approved;
public Task<IOrderState> ShipAsync(Order order, CancellationToken ct) =>
Task.FromResult<IOrderState>(new ShippedState());
public Task<IOrderState> CancelAsync(
Order order, string reason, CancellationToken ct) =>
Task.FromResult<IOrderState>(new CancelledState());
public Task<IOrderState> SubmitAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Already approved");
public Task<IOrderState> ApproveAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Already approved");
public Task<IOrderState> DeliverAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Must ship before delivering");
}
public class ShippedState : IOrderState
{
public OrderStatus Status => OrderStatus.Shipped;
public Task<IOrderState> DeliverAsync(Order order, CancellationToken ct) =>
Task.FromResult<IOrderState>(new DeliveredState());
public Task<IOrderState> SubmitAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Already shipped");
public Task<IOrderState> ApproveAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Already shipped");
public Task<IOrderState> ShipAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Already shipped");
public Task<IOrderState> CancelAsync(
Order order, string reason, CancellationToken ct) =>
throw new InvalidOperationException("Cannot cancel shipped order");
}
public class DeliveredState : IOrderState
{
public OrderStatus Status => OrderStatus.Delivered;
public Task<IOrderState> SubmitAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Order already delivered");
public Task<IOrderState> ApproveAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Order already delivered");
public Task<IOrderState> ShipAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Order already delivered");
public Task<IOrderState> DeliverAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Order already delivered");
public Task<IOrderState> CancelAsync(
Order order, string reason, CancellationToken ct) =>
throw new InvalidOperationException("Cannot cancel delivered order");
}
public class CancelledState : IOrderState
{
public OrderStatus Status => OrderStatus.Cancelled;
public Task<IOrderState> SubmitAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Order is cancelled");
public Task<IOrderState> ApproveAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Order is cancelled");
public Task<IOrderState> ShipAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Order is cancelled");
public Task<IOrderState> DeliverAsync(Order order, CancellationToken ct) =>
throw new InvalidOperationException("Order is cancelled");
public Task<IOrderState> CancelAsync(
Order order, string reason, CancellationToken ct) =>
throw new InvalidOperationException("Order is already cancelled");
}State Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
State Pattern in the Real World
“Think of a traffic light. The light itself (context) doesn't change its wiring, but its active state—red, yellow, or green—completely determines what drivers should do. Each color has its own rules, and the light transitions through states on a timer. Adding a flashing-yellow state only requires defining that state's rules, not rewiring the entire light.”