Chain of Responsibility in C# Request Pipelines

This article explains how to implement the Chain of Responsibility design pattern to build flexible, decoupled request-handling pipelines in C#. You will learn the core architectural components of the pattern, see a step-by-step code implementation using an abstract base handler, and explore how to wire handlers together dynamically to process, validate, or terminate incoming requests.


Core Concept

The Chain of Responsibility is a behavioral design pattern that passes a request along a sequence of handlers. Upon receiving a request, each handler decides either to process it and pass it to the next handler in the chain, or to halt execution (short-circuit) if validation or conditions fail.

A standard implementation consists of three primary elements:

  1. Request Object: The data or context passed through the chain.
  2. Handler Interface/Base Class: The contract defining how handlers link to one another and process requests.
  3. Concrete Handlers: Individual classes containing specific processing logic (e.g., authentication, logging, validation).

Step 1: Define the Request Context

Create a request class that encapsulates the data passing through the pipeline.

public class OrderRequest
{
    public int OrderId { get; set; }
    public decimal Amount { get; set; }
    public string UserRole { get; set; } = string.Empty;
    public bool IsProcessed { get; set; }
}

Step 2: Create the Handler Abstraction

Define an interface or an abstract base class. An abstract base class is often preferred because it can manage the reference to the next handler and enforce default propagation behavior.

public interface IOrderHandler
{
    IOrderHandler SetNext(IOrderHandler nextHandler);
    void Handle(OrderRequest request);
}

public abstract class BaseOrderHandler : IOrderHandler
{
    private IOrderHandler? _nextHandler;

    public IOrderHandler SetNext(IOrderHandler nextHandler)
    {
        _nextHandler = nextHandler;
        return nextHandler; // Allows fluent chaining: h1.SetNext(h2).SetNext(h3)
    }

    public virtual void Handle(OrderRequest request)
    {
        // Default behavior: pass to the next handler if it exists
        _nextHandler?.Handle(request);
    }
}

Step 3: Implement Concrete Handlers

Each concrete handler encapsulates a single responsibility and decides whether to continue execution.

Authentication/Authorization Handler

public class AuthorizationHandler : BaseOrderHandler
{
    public override void Handle(OrderRequest request)
    {
        Console.WriteLine("Executing AuthorizationHandler...");

        if (request.UserRole != "Admin" && request.UserRole != "Customer")
        {
            Console.WriteLine("Pipeline Terminated: Unauthorized user role.");
            return; // Short-circuit: Do not call base.Handle(request)
        }

        base.Handle(request);
    }
}

Validation Handler

public class ValidationHandler : BaseOrderHandler
{
    public override void Handle(OrderRequest request)
    {
        Console.WriteLine("Executing ValidationHandler...");

        if (request.Amount <= 0)
        {
            Console.WriteLine("Pipeline Terminated: Order amount must be greater than zero.");
            return; // Short-circuit
        }

        base.Handle(request);
    }
}

Business Logic Handler

public class PaymentProcessingHandler : BaseOrderHandler
{
    public override void Handle(OrderRequest request)
    {
        Console.WriteLine("Executing PaymentProcessingHandler...");

        request.IsProcessed = true;
        Console.WriteLine($"Payment of {request.Amount:C} processed successfully for Order #{request.OrderId}.");

        base.Handle(request);
    }
}

Step 4: Assemble and Execute the Pipeline

Link the handlers in the required execution sequence and pass the request into the head of the chain.

public class Program
{
    public static void Main()
    {
        // 1. Instantiate handlers
        var authHandler = new AuthorizationHandler();
        var validationHandler = new ValidationHandler();
        var paymentHandler = new PaymentProcessingHandler();

        // 2. Build the pipeline: Auth -> Validation -> Payment
        authHandler
            .SetNext(validationHandler)
            .SetNext(paymentHandler);

        // 3. Test a valid request
        Console.WriteLine("--- Processing Valid Request ---");
        var validRequest = new OrderRequest
        {
            OrderId = 101,
            Amount = 150.00m,
            UserRole = "Customer"
        };
        authHandler.Handle(validRequest);

        // 4. Test an invalid request (short-circuit demonstration)
        Console.WriteLine("\n--- Processing Invalid Request ---");
        var invalidRequest = new OrderRequest
        {
            OrderId = 102,
            Amount = -25.00m,
            UserRole = "Customer"
        };
        authHandler.Handle(invalidRequest);
    }
}

Key Benefits

  • Single Responsibility Principle: Each handler manages only one specific operation (e.g., authentication, logging, validation).
  • Open/Closed Principle: New handlers can be introduced into the pipeline without modifying existing handler logic or the client calling the pipeline.
  • Dynamic Reconfiguration: Handlers can be arranged, added, or removed at runtime depending on configuration or environment settings.