Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Guided course · Architecture and Design in .NET

    Design Patterns in C# with Practical Examples: back to the course

    Module 4 · 4. Behavioral Patterns and Interview Practice · Lesson 11 of 12

    Command, State, and Chain of Responsibility

    Learning outcome

    You will be able to: (1) encapsulate requests as Command objects so callers are decoupled from processing, (2) model context-dependent behaviour via the State pattern, and (3) compose modular processing steps with Chain of Responsibility so handlers can validate, log, and execute changes with clear separation of concerns.

    Intuition

    • Command: wrap a request (name + data + logic) into an object so you can queue, log, undo, and pass it to pipelines without tight coupling.
    • State: store the Context's state as an object so behaviour changes by swapping the state instance rather than sprinkling conditionals across handlers.
    • Chain of Responsibility: a linked pipeline of handlers that can choose to process, short-circuit, or pass along; good for validation, auth, logging, and transformation.

    Deep dive

    Key implementation roles:

    • ICommand: small object representing a request; contains the data and minimal execute metadata.
    • Handler (chain): receives ICommand + Context, performs a focused responsibility (validate/log/execute), and forwards to Next.
    • Context + IState: represents operational mode; commands may cause transitions by asking the current state to handle a transition.

    Why combine them? Commands decouple who asked from what is done; Chain composes orthogonal concerns (validation, tracing, business execution); State keeps business rules for transitions localized and testable. This makes pipelines extensible without modifying core business code.

    Example pseudocode usage (conceptual):

    Failure modes

    • Over-chaining: creating many tiny handlers for trivial systems increases indirection and makes debugging harder.
    • State explosion: modeling every combination of behaviour as a state object can become unwieldy; group related transitions and use predicates to limit states.
    • Order-sensitivity bugs: Chain ordering matters (e.g., logging before validation may log invalid requests). Document expected ordering.
    • Hidden side effects: Handlers that mutate context should do it explicitly and document transitions; prefer returning transition intents from business handlers rather than performing many in-flight mutations.

    Interview drill

    Q1: Why choose Command+Chain instead of a single service method? (Look for decoupling, testability, pipeline composition.) Q2: When would State be overkill? (Systems with only 1–2 boolean flags or small behavior differences.) Q3: How do you test a pipeline? (Unit-test handlers in isolation; use an in-memory context for integration tests; inject a terminal test handler.)

    Micro-test (mental): Given handlers [Validation, Logging, Business], what happens if Validation rejects? (Expected: short-circuit; Logging and Business not executed.)

    Revision checklist

    • Commands are immutable or carry minimal mutable envelope.
    • Handlers are single-responsibility and chainable.
    • States encapsulate transition rules and are swapped on well-defined points.
    • Side effects are minimized and documented.
    • Ordering of handlers is well-documented and covered by tests.

    Production code

    When to prefer this composition over a simpler approach:

    • Prefer when multiple orthogonal concerns must run around business logic (auth, validation, logging, metrics), and those concerns change independently.
    • Avoid for microservices with a single, stable request flow; a direct service call is simpler.

    Misuse warning: Don't use this pattern to hide large synchronous transactions across many handlers — prefer explicit orchestration or sagas for multi-step distributed work.

    Below is a compact, deterministic, production-minded C# console example that materially demonstrates all three patterns together. It includes a classic extension method to run a Command through a chain and a comment showing how a future C# "extension(...)" block might express the same intent. The program prints deterministic lines so it is easy to test and reason about.

    Code walkthrough

    • ICommand: request object with Name (for deterministic logging).
    • Context + IState: initial IdleState, ProcessingState, CompletedState; business handler asks current state whether a command can trigger a transition.
    • Handlers: ValidationHandler, LoggingHandler, BusinessHandler form a chain. Validation always passes in this example to keep output deterministic.
    • Extension method ExecuteThrough wires a command to a chain head for clarity.

    Important: Every line of console output in the example is deterministic so the example is suitable for interview micro-tests and automated checks.

    Below is the runnable source included in the exercise. Note the commented section that illustrates a hypothetical C# extension(...) block syntax (version-sensitive); the code uses a classic extension method for broad compatibility.

    Exercises with explanations

    1) Add an AuthorizationHandler that rejects commands when the context lacks roles. Test that Authorization short-circuits later handlers. Explanation: shows ordering importance. 2) Add a RetryHandler that re-invokes the terminal BusinessHandler on transient failures. Explanation: shows how a Chain can implement resilience. 3) Refactor state transitions so BusinessHandler returns Transition objects (instead of mutating context) and a coordinator applies them. Explanation: improves testability and separates concerns.

    Interview: ask the candidate to extend the sample to support queuing commands and replaying them against a new Context instance to test idempotence.

    Executable code examples

    Command-State-Chain Pipeline Demo

    Program.cs

    C#Runs
    using System;
    // Why Command + State + Chain is preferable here: commands are decoupled, pipeline stages compose independently, and transitions are explicit and testable.
    // Misuse warning: for one stable service call without independent stages or state transitions, this composition is unnecessary overhead.
    
    // Small, deterministic demo combining Command, State, and Chain of Responsibility.
    
    // ICommand: minimal request object
    interface ICommand
    {
        string Name { get; }
    }
    
    record SetupCommand() : ICommand { public string Name => "Setup"; }
    record WorkCommand() : ICommand { public string Name => "Work"; }
    record FinishCommand() : ICommand { public string Name => "Finish"; }
    
    // Context + State
    class Context
    {
        private IState _state;
        public Context(IState initial) => _state = initial;
        public string StateName => _state.Name;
    
        // Called by business logic to ask the state to handle a transition
        public void ApplyTransition(Func<IState, IState?> transition)
        {
            var next = transition(_state);
            if (next is not null)
                _state = next;
        }
    }
    
    interface IState { string Name { get; } }
    class IdleState : IState { public string Name => "Idle"; }
    class ProcessingState : IState { public string Name => "Processing"; }
    class CompletedState : IState { public string Name => "Completed"; }
    
    // Chain of Responsibility base
    abstract class Handler
    {
        private Handler? _next;
    
        public Handler? SetNext(Handler next)
        {
            _next = next;
            return next;
        }
    
        public void Handle(ICommand cmd, Context ctx)
        {
            if (!Process(cmd, ctx))
                return; // short-circuit
    
            _next?.Handle(cmd, ctx);
        }
    
        // Return true if chain should continue
        protected abstract bool Process(ICommand cmd, Context ctx);
    }
    
    class ValidationHandler : Handler
    {
        protected override bool Process(ICommand cmd, Context ctx)
        {
            // Deterministic: everything passes
            Console.WriteLine($"ValidationHandler: {cmd.Name} validation OK");
            return true;
        }
    }
    
    class LoggingHandler : Handler
    {
        protected override bool Process(ICommand cmd, Context ctx)
        {
            Console.WriteLine($"LoggingHandler: {cmd.Name} logged");
            return true;
        }
    }
    
    class BusinessHandler : Handler
    {
        protected override bool Process(ICommand cmd, Context ctx)
        {
            // Business logic inspects command and may ask context to transition state
            if (cmd is SetupCommand)
            {
                Console.WriteLine($"BusinessHandler: {cmd.Name} executed, transitioned {ctx.StateName}->Processing");
                ctx.ApplyTransition(s => s is IdleState ? new ProcessingState() : null);
            }
            else if (cmd is WorkCommand)
            {
                Console.WriteLine($"BusinessHandler: {cmd.Name} executed in {ctx.StateName}");
                // no transition
            }
            else if (cmd is FinishCommand)
            {
                Console.WriteLine($"BusinessHandler: {cmd.Name} executed, transitioned {ctx.StateName}->Completed");
                ctx.ApplyTransition(s => s is ProcessingState ? new CompletedState() : null);
            }
            else
            {
                Console.WriteLine($"BusinessHandler: {cmd.Name} unknown - no-op");
            }
    
            return true; // continue if there were further handlers
        }
    }
    
    // Classic extension method to execute a command through a chain head.
    internal static class PipelineExtensions
    {
        internal static void ExecuteThrough(this ICommand cmd, Handler head, Context ctx)
        {
            head.Handle(cmd, ctx);
        }
    }
    
    class Program
    {
        static void Main()
        {
            // Build a chain: Validation -> Logging -> Business
            var validation = new ValidationHandler();
            var logging = new LoggingHandler();
            var business = new BusinessHandler();
            validation.SetNext(logging).SetNext(business);
    
            var context = new Context(new IdleState());
    
            Console.WriteLine("=== Pipeline Run ===");
    
            var commands = new ICommand[]
            {
                new SetupCommand(),
                new WorkCommand(),
                new FinishCommand()
            };
    
            foreach (var cmd in commands)
            {
                Console.WriteLine($"-- Command: {cmd.Name}");
                cmd.ExecuteThrough(validation, context); // extension method
            }
    
            Console.WriteLine($"Final State: {context.StateName}");
        }
    }
    Sign in to mark lessons done and keep your place in the course.Sign in