BrandGhost
How to Implement Chain of Responsibility Pattern in C#: Step-by-Step Guide

How to Implement Chain of Responsibility Pattern in C#: Step-by-Step Guide

How to Implement Chain of Responsibility Pattern in C#: Step-by-Step Guide

Passing a request down a chain of handlers until one of them deals with it is a deceptively simple idea -- but getting the implementation right takes deliberate structure. The chain of responsibility pattern is a behavioral design pattern that decouples the sender of a request from its receivers by giving multiple objects a chance to handle it. If you want to implement chain of responsibility pattern in C#, this guide takes you through every step -- from defining a handler abstraction to wiring chains into a dependency injection container with logging. By the end, you'll have production-ready code you can compile and adapt for your own pipelines.

We'll build progressively. First a handler interface, then an abstract base class that reduces boilerplate, then concrete handlers for a purchase approval workflow, chain construction, DI integration, and finally diagnostics. Each step includes complete C# code that compiles and runs.

Step 1: Define the Handler Interface or Base Class

The foundation of any chain of responsibility implementation is the handler abstraction. You have two options here -- an interface-based approach and an abstract class approach. Both work, but they differ in how much repetitive code you write.

Interface-Based Approach

Starting with an interface gives you maximum flexibility. It defines the contract without imposing any implementation details:

public interface IHandler<TRequest>
{
    IHandler<TRequest>? NextHandler { get; }

    void SetNext(IHandler<TRequest> next);

    void Handle(TRequest request);
}

Every concrete handler must implement all three members. The SetNext method links one handler to the next in the chain, and Handle processes the request or passes it along. The generic TRequest parameter lets you implement chain of responsibility pattern in C# for any request type without casting.

This interface is clean, but it forces every handler to re-implement the chain-passing logic. If you have ten handlers, that's ten nearly identical SetNext implementations and ten Handle methods that each check whether to pass to the next handler.

Abstract Class Approach

An abstract base class eliminates that repetition by providing default chain-passing behavior:

public abstract class HandlerBase<TRequest>
{
    private HandlerBase<TRequest>? _nextHandler;

    public void SetNext(HandlerBase<TRequest> next)
    {
        _nextHandler = next;
    }

    public void Handle(TRequest request)
    {
        if (CanHandle(request))
        {
            Process(request);
            return;
        }

        if (_nextHandler is not null)
        {
            _nextHandler.Handle(request);
            return;
        }

        HandleUnprocessed(request);
    }

    protected abstract bool CanHandle(TRequest request);

    protected abstract void Process(TRequest request);

    protected virtual void HandleUnprocessed(
        TRequest request)
    {
        Console.WriteLine(
            "[Chain] Request reached end of chain " +
            "without being handled.");
    }
}

The base class manages the chain linkage and delegates decisions to two abstract methods -- CanHandle and Process. Concrete handlers only need to answer two questions: can I handle this request, and what do I do with it? The HandleUnprocessed virtual method provides a default behavior when no handler in the chain can process the request, but subclasses can override it.

This approach is the one I'd recommend in most cases. It follows the same structural principle you'll find in the template method design pattern, where a base class defines the algorithm skeleton and subclasses fill in the details. The chain-walking logic lives in one place, and concrete handlers stay focused.

Step 2: Implement Concrete Handlers

With the base class in place, we can build concrete handlers for a realistic scenario. We'll model a purchase approval chain where different levels of authority can approve requests up to certain amounts. This is a classic example when you implement chain of responsibility pattern in C# because each handler has a clear, distinct responsibility.

First, define the request:

public sealed class PurchaseRequest
{
    public string Description { get; }

    public decimal Amount { get; }

    public string RequestedBy { get; }

    public PurchaseRequest(
        string description,
        decimal amount,
        string requestedBy)
    {
        Description = description;
        Amount = amount;
        RequestedBy = requestedBy;
    }

    public override string ToString()
    {
        return $"{Description} (${Amount:N2}) " +
            $"by {RequestedBy}";
    }
}

Now the handlers. Each one checks whether the purchase amount falls within its approval authority:

ManagerHandler

public sealed class ManagerHandler
    : HandlerBase<PurchaseRequest>
{
    private const decimal ApprovalLimit = 1_000m;

    protected override bool CanHandle(
        PurchaseRequest request)
    {
        return request.Amount <= ApprovalLimit;
    }

    protected override void Process(
        PurchaseRequest request)
    {
        Console.WriteLine(
            $"[Manager] Approved: {request}");
    }
}

DirectorHandler

public sealed class DirectorHandler
    : HandlerBase<PurchaseRequest>
{
    private const decimal ApprovalLimit = 10_000m;

    protected override bool CanHandle(
        PurchaseRequest request)
    {
        return request.Amount <= ApprovalLimit;
    }

    protected override void Process(
        PurchaseRequest request)
    {
        Console.WriteLine(
            $"[Director] Approved: {request}");
    }
}

VPHandler

public sealed class VPHandler
    : HandlerBase<PurchaseRequest>
{
    private const decimal ApprovalLimit = 50_000m;

    protected override bool CanHandle(
        PurchaseRequest request)
    {
        return request.Amount <= ApprovalLimit;
    }

    protected override void Process(
        PurchaseRequest request)
    {
        Console.WriteLine(
            $"[VP] Approved: {request}");
    }
}

CEOHandler

public sealed class CEOHandler
    : HandlerBase<PurchaseRequest>
{
    protected override bool CanHandle(
        PurchaseRequest request)
    {
        // CEO can approve any amount
        return true;
    }

    protected override void Process(
        PurchaseRequest request)
    {
        Console.WriteLine(
            $"[CEO] Approved: {request}");
    }
}

A few things worth noting about these handlers when you implement chain of responsibility pattern in C#:

  • Each handler has a single, clear condition for handling a request. The ManagerHandler approves anything up to $1,000, the DirectorHandler up to $10,000, and so on.
  • The CEOHandler serves as the terminal handler -- it always returns true from CanHandle, so nothing falls through the chain unprocessed.
  • None of these handlers know about each other. The ManagerHandler has no idea that a DirectorHandler exists. That decoupling is the entire point of the pattern.

This separation of concerns is similar to how the strategy design pattern encapsulates interchangeable algorithms. The difference is that strategies are selected explicitly by the caller, while chain handlers self-select based on the request.

Step 3: Build the Chain

With handlers implemented, the next step is linking them together. There are two approaches -- manual linking and a fluent builder.

Manual Linking with SetNext

The most straightforward way to build a chain is to call SetNext on each handler in sequence:

var manager = new ManagerHandler();
var director = new DirectorHandler();
var vp = new VPHandler();
var ceo = new CEOHandler();

manager.SetNext(director);
director.SetNext(vp);
vp.SetNext(ceo);

// Use the chain
manager.Handle(new PurchaseRequest(
    "Office supplies", 500m, "Alice"));
// [Manager] Approved: Office supplies ($500.00) by Alice

manager.Handle(new PurchaseRequest(
    "Server hardware", 8_000m, "Bob"));
// [Director] Approved: Server hardware ($8,000.00) by Bob

manager.Handle(new PurchaseRequest(
    "Company retreat", 75_000m, "Carol"));
// [CEO] Approved: Company retreat ($75,000.00) by Carol

This works, but it's error-prone. If you forget a SetNext call or wire the order wrong, the chain breaks silently. For a chain with four handlers that's manageable, but as chains grow, manual linking becomes a maintenance burden.

Builder Pattern for Fluent Chain Construction

A builder encapsulates chain construction and enforces correct linkage:

public sealed class ChainBuilder<TRequest>
{
    private readonly List<HandlerBase<TRequest>> _handlers
        = new();

    public ChainBuilder<TRequest> AddHandler(
        HandlerBase<TRequest> handler)
    {
        _handlers.Add(handler);
        return this;
    }

    public HandlerBase<TRequest> Build()
    {
        if (_handlers.Count == 0)
        {
            throw new InvalidOperationException(
                "Chain must contain at least one handler.");
        }

        for (int i = 0; i < _handlers.Count - 1; i++)
        {
            _handlers[i].SetNext(_handlers[i + 1]);
        }

        return _handlers[0];
    }
}

The builder collects handlers in order and links them during Build. The return value is the first handler in the chain -- the entry point for all requests. Here's how to use it:

var chain = new ChainBuilder<PurchaseRequest>()
    .AddHandler(new ManagerHandler())
    .AddHandler(new DirectorHandler())
    .AddHandler(new VPHandler())
    .AddHandler(new CEOHandler())
    .Build();

chain.Handle(new PurchaseRequest(
    "Team lunch", 250m, "Dave"));
// [Manager] Approved: Team lunch ($250.00) by Dave

chain.Handle(new PurchaseRequest(
    "Annual license", 25_000m, "Eve"));
// [VP] Approved: Annual license ($25,000.00) by Eve

This fluent API makes chain construction readable and hard to get wrong. The builder handles all the linking internally, so you can't accidentally skip a connection. When you implement chain of responsibility pattern in C# for production code, a builder like this is well worth the small upfront investment.

Step 4: Wire Up with Dependency Injection

For real applications, you'll want to register handlers in a DI container and resolve the chain through a factory. When you implement chain of responsibility pattern in C# with IServiceCollection, the factory handles chain construction while the container manages handler lifetimes.

First, register individual handlers:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddSingleton<ManagerHandler>();
services.AddSingleton<DirectorHandler>();
services.AddSingleton<VPHandler>();
services.AddSingleton<CEOHandler>();

Next, register a factory that builds the chain from resolved handlers:

services.AddSingleton<HandlerBase<PurchaseRequest>>(
    provider =>
    {
        var chain =
            new ChainBuilder<PurchaseRequest>()
            .AddHandler(
                provider.GetRequiredService<ManagerHandler>())
            .AddHandler(
                provider.GetRequiredService<DirectorHandler>())
            .AddHandler(
                provider.GetRequiredService<VPHandler>())
            .AddHandler(
                provider.GetRequiredService<CEOHandler>())
            .Build();

        return chain;
    });

Consuming code resolves the chain as a single HandlerBase<PurchaseRequest> dependency:

var provider = services.BuildServiceProvider();

var approvalChain = provider
    .GetRequiredService<HandlerBase<PurchaseRequest>>();

approvalChain.Handle(new PurchaseRequest(
    "Cloud subscription", 3_500m, "Frank"));
// [Director] Approved: Cloud subscription ($3,500.00) by Frank

The consuming class never knows how many handlers exist or what order they run in. It sees a single entry point. That's inversion of control in action -- the container owns the composition, and the consumer depends on the abstraction.

This approach also makes testing straightforward. In unit tests, you can construct a chain with only the handlers relevant to the test scenario. Need to verify that requests above $10,000 bypass the director? Build a chain with just DirectorHandler and VPHandler and check the output.

Step 5: Add Logging and Diagnostics

When you implement chain of responsibility pattern in C# for production systems, you'll inevitably need to debug why a request was or wasn't handled. Adding logging directly into the base class catches every handler invocation without modifying individual handlers.

Update the HandlerBase class to include diagnostic output:

public abstract class LoggingHandlerBase<TRequest>
{
    private LoggingHandlerBase<TRequest>? _nextHandler;

    public abstract string HandlerName { get; }

    public void SetNext(
        LoggingHandlerBase<TRequest> next)
    {
        _nextHandler = next;
    }

    public void Handle(TRequest request)
    {
        Console.WriteLine(
            $"[Chain] {HandlerName} evaluating " +
            $"request: {request}");

        if (CanHandle(request))
        {
            Console.WriteLine(
                $"[Chain] {HandlerName} HANDLING " +
                $"request.");
            Process(request);
            return;
        }

        Console.WriteLine(
            $"[Chain] {HandlerName} PASSING to " +
            $"next handler.");

        if (_nextHandler is not null)
        {
            _nextHandler.Handle(request);
            return;
        }

        Console.WriteLine(
            $"[Chain] End of chain reached. " +
            $"No handler processed the request.");
        HandleUnprocessed(request);
    }

    protected abstract bool CanHandle(TRequest request);

    protected abstract void Process(TRequest request);

    protected virtual void HandleUnprocessed(
        TRequest request)
    {
    }
}

With this base class, every handler automatically logs whether it evaluated the request, handled it, or passed it along. The output for a $25,000 request through the full chain looks like this:

[Chain] Manager evaluating request: Annual license ($25,000.00) by Eve
[Chain] Manager PASSING to next handler.
[Chain] Director evaluating request: Annual license ($25,000.00) by Eve
[Chain] Director PASSING to next handler.
[Chain] VP HANDLING request.
[VP] Approved: Annual license ($25,000.00) by Eve

In a production system, you'd replace Console.WriteLine with an ILogger<T> instance injected into the base class. The structural pattern stays identical -- the base class wraps each handler's evaluation with entry/exit logging, and concrete handlers remain unaware of the diagnostics layer.

This pattern of wrapping behavior around a core operation is closely related to the decorator design pattern. The difference is that here the logging lives in the base class itself rather than in a separate wrapper. For more granular control -- say, adding timing metrics or retry logic to specific handlers -- a decorator approach on top of the chain would give you that flexibility without touching the base class.

Common Implementation Mistakes

Even experienced developers run into these issues when they implement chain of responsibility pattern in C#.

Forgetting the null check on the next handler: If the last handler in the chain calls _nextHandler.Handle(request) without checking for null, you'll get a NullReferenceException at runtime. The base class approach solves this by centralizing the null check, but if you're using the interface approach, every handler must guard against a missing next handler.

Creating circular chains: If handler A points to handler B and handler B points back to handler A, you've created an infinite loop. The chain will recurse until you hit a StackOverflowException. The builder pattern helps prevent this by constructing chains linearly, but manual SetNext calls can easily introduce cycles if you're not careful.

Chains with too many handlers: A chain with 20 handlers is hard to reason about. If a request falls through 15 handlers before being processed, debugging becomes painful even with logging. Consider whether some handlers can be consolidated, or whether a different pattern -- like the command design pattern with a dispatch table -- would be more appropriate for your use case.

Handlers with unintended side effects: If a handler modifies the request object or changes shared state before deciding it can't handle the request, downstream handlers receive corrupted data. When you implement chain of responsibility pattern in C#, handlers that pass on a request should leave it exactly as they found it. If a handler needs to enrich or transform the request, that should only happen as part of actually handling it.

Frequently Asked Questions

What is the chain of responsibility pattern and when should I use it?

The chain of responsibility pattern is a behavioral design pattern that lets you pass a request along a chain of handlers, where each handler decides whether to process the request or pass it to the next one. You should use it when multiple objects might handle a request and you don't want the sender to know which one does. It's particularly useful for approval workflows, middleware pipelines, event handling systems, and validation chains where the processing logic varies based on the request's characteristics.

How does the chain of responsibility pattern differ from the strategy pattern?

The strategy pattern requires the caller to explicitly select which algorithm to use. The chain of responsibility pattern lets the handlers themselves decide. With strategy, you pick the handler up front. With chain of responsibility, you submit the request and the chain figures out who handles it. Use strategy when you know which behavior you want. Use chain of responsibility when the selection logic is distributed across handlers.

Can I implement chain of responsibility pattern in C# with async handlers?

Yes. Change the Handle method to return Task and make CanHandle and Process async as well. The chain-walking logic in the base class becomes await _nextHandler.Handle(request) instead of a synchronous call. Be mindful of exception handling in async chains -- if a handler throws, you need to decide whether the chain should continue or abort. A try-catch in the base class's Handle method gives you a centralized place to make that decision.

How do I test a chain of responsibility implementation?

Test each handler in isolation first. Create a single handler, feed it requests that it should and shouldn't handle, and verify the behavior. Then test chain composition by building small chains -- two or three handlers -- and confirming that requests route correctly. You can verify which handler processed a request by checking output, using a mock logger, or having Process set a flag on a test-specific request object. The adapter design pattern can also help when you need to wrap legacy validation logic into a handler interface for testing purposes.

Should I use an interface or abstract class for the handler?

For most scenarios, the abstract class is the better choice. It centralizes chain-walking logic, eliminates repetitive SetNext implementations, and provides a default end-of-chain behavior. Use an interface when you need handlers that already inherit from another class (since C# doesn't support multiple inheritance), or when you want maximum flexibility and don't mind the extra boilerplate in each handler.

What happens when no handler in the chain processes a request?

That depends on your implementation. The base class shown in this guide calls HandleUnprocessed, which you can override per handler or leave as a default no-op. Common strategies include throwing an exception, logging a warning, returning a default response, or routing to a fallback handler. The best approach depends on your domain. In the purchase approval example, you might reject the request. In a middleware pipeline, you might return a default response.

How does chain of responsibility compare to middleware in ASP.NET Core?

ASP.NET Core's middleware pipeline is a chain of responsibility implementation. Each middleware component receives the request, optionally processes it, and calls next() to pass it along. The main difference is structural -- ASP.NET Core uses delegates and a builder API rather than explicit handler classes. The underlying concept is identical: a sequence of processors where each one decides whether to handle the request or pass it to the next. When you implement chain of responsibility pattern in C# using the handler class approach, you get more explicit control over handler composition and lifetime management compared to the delegate-based middleware model.

Chain of Responsibility Pattern in C# - Simplified How-To Guide

Use the Chain of Responsibility pattern in C# to streamline your application architecture. Discover the benefits, key components, and best practices!

Chain of Responsibility Design Pattern in C#: Complete Guide with Examples

Master the chain of responsibility design pattern in C# with practical examples showing handler chains, middleware pipelines, and real-world .NET implementations.

How to Implement Strategy Pattern in C#: Step-by-Step Guide

How to implement Strategy pattern in C#: step-by-step guide with code examples, best practices, and common pitfalls for behavioral design patterns.

An error has occurred. This application may no longer respond until reloaded. Reload