Chain of Responsibility Design Pattern in C#: Complete Guide with Examples
When you need to pass a request through a series of processors where each one decides whether to handle it, forward it, or both, the chain of responsibility design pattern in C# is the behavioral pattern designed for exactly that job. It decouples the sender of a request from its receivers by giving multiple objects a chance to process the request along a linked chain. Instead of hardwiring a single handler, you build a pipeline -- and that pipeline can be extended, reordered, or reconfigured without touching the sender or any individual handler.
In this guide, we'll cover the chain of responsibility design pattern from the core components and a basic C# implementation through ASP.NET Core middleware, dependency injection, and practical guidance on when to use it.
What Is the Chain of Responsibility Design Pattern?
The chain of responsibility design pattern is a behavioral pattern from the Gang of Four (GoF) catalog that lets you send a request along a chain of handlers. Each handler in the chain examines the request and either processes it, passes it to the next handler, or does both. The sender never knows which handler ultimately processes the request -- it only knows the first handler in the chain.
Think about a support ticket system. A customer submits a ticket, and it first goes to a front-line agent. If the agent can solve it, great -- the chain stops. If not, it gets escalated to a specialist. If the specialist can't handle it either, it moves up to a manager. The customer doesn't care who resolves the ticket, and neither does the system that created it. The chain handles the routing.
This decoupling solves a real problem: without the chain of responsibility pattern, the sender would need conditional logic to determine the correct handler, creating tight coupling and a violation of the open/closed principle. With the pattern, adding a new handler means inserting a new link in the chain -- no existing code changes required.
Chain Variants
Not every chain works the same way, and understanding the variants helps you pick the right one for your situation.
The pass or handle variant is the classic form. Each handler either processes the request entirely and stops the chain, or passes it to the next handler untouched. This works well for scenarios like exception handling or routing, where exactly one handler should process each request.
The handle and continue variant lets every handler in the chain process the request, regardless of what the previous handlers did. Logging and audit pipelines use this approach -- every handler adds its contribution and forwards the request along.
The handle and decide variant gives each handler the flexibility to process the request and then decide whether to continue the chain or stop it. This is the most flexible form and maps directly to how ASP.NET Core middleware works. Each middleware component can do work before and after calling next, and it can short-circuit the pipeline by not calling next at all.
Core Components of the Chain of Responsibility Pattern
The chain of responsibility design pattern in C# involves three key participants that work together to route requests through the chain.
The Handler is an abstract base class or interface that defines the contract for handling requests and holding a reference to the next handler in the chain. It typically provides a SetNext method (or accepts the next handler through its constructor) and declares a Handle method that concrete handlers override. The base handler often includes default behavior that forwards the request to the next handler if one exists.
The Concrete Handlers implement the handler interface and contain the actual processing logic. Each concrete handler decides whether it can handle the request. If it can, it processes it. If it can't (or if it processes and still wants to continue), it delegates to the next handler in the chain. The beauty is that each handler is self-contained -- it knows about its own responsibility and the next handler, nothing more.
The Client constructs the chain and sends requests to the first handler. Once the chain is built, the client sends requests to the head of the chain and doesn't concern itself with which handler processes them. This is where dependency injection can help manage the wiring in larger applications.
Implementing the Chain of Responsibility Pattern in C#
Let's build a practical example: a request validation pipeline that processes incoming API requests through authentication, authorization, and input validation steps. This is a scenario where the chain of responsibility design pattern in C# shines because each handler has a distinct concern and the order matters.
Defining the Handler Base Class
The abstract handler provides the chain-linking infrastructure and a default forwarding behavior:
public abstract class RequestHandler
{
private RequestHandler? _nextHandler;
public RequestHandler SetNext(RequestHandler handler)
{
_nextHandler = handler;
return handler;
}
public virtual bool Handle(Request request)
{
if (_nextHandler is not null)
{
return _nextHandler.Handle(request);
}
// End of chain -- request passed all handlers
return true;
}
}
public sealed class Request
{
public string? AuthToken { get; set; }
public string? UserRole { get; set; }
public string? Body { get; set; }
public List<string> ValidationErrors { get; } = new();
}
The SetNext method returns the handler it receives, enabling fluent chaining. The base Handle method forwards to the next handler if one exists, or returns true if the chain is exhausted.
Building Concrete Handlers
Each handler focuses on one concern. Here's the authentication handler:
using System;
public sealed class AuthenticationHandler : RequestHandler
{
public override bool Handle(Request request)
{
if (string.IsNullOrEmpty(request.AuthToken))
{
Console.WriteLine(
"AuthenticationHandler: " +
"No auth token provided. Rejected.");
return false;
}
if (request.AuthToken != "valid-token-123")
{
Console.WriteLine(
"AuthenticationHandler: " +
"Invalid auth token. Rejected.");
return false;
}
Console.WriteLine(
"AuthenticationHandler: " +
"Token validated. Passing to next handler.");
return base.Handle(request);
}
}
If the token is missing or invalid, the handler rejects the request and the chain stops. If the token is valid, it calls base.Handle(request) to forward to the next handler. Here's the authorization handler:
using System;
public sealed class AuthorizationHandler : RequestHandler
{
private readonly string _requiredRole;
public AuthorizationHandler(string requiredRole)
{
_requiredRole = requiredRole;
}
public override bool Handle(Request request)
{
if (request.UserRole != _requiredRole)
{
Console.WriteLine(
$"AuthorizationHandler: " +
$"User lacks '{_requiredRole}' role. Rejected.");
return false;
}
Console.WriteLine(
"AuthorizationHandler: " +
"Role verified. Passing to next handler.");
return base.Handle(request);
}
}
And the validation handler that checks the request body:
using System;
public sealed class ValidationHandler : RequestHandler
{
public override bool Handle(Request request)
{
if (string.IsNullOrWhiteSpace(request.Body))
{
Console.WriteLine(
"ValidationHandler: " +
"Request body is empty. Rejected.");
return false;
}
if (request.Body.Length > 1000)
{
Console.WriteLine(
"ValidationHandler: " +
"Request body exceeds limit. Rejected.");
return false;
}
Console.WriteLine(
"ValidationHandler: " +
"Body validated. Passing to next handler.");
return base.Handle(request);
}
}
Each handler has a single responsibility and knows nothing about the other handlers in the chain. The chain of responsibility design pattern in C# keeps each handler focused on its own concern.
Constructing and Running the Chain
Here's how the client assembles the chain and processes requests:
using System;
var authentication = new AuthenticationHandler();
var authorization = new AuthorizationHandler("admin");
var validation = new ValidationHandler();
// Build the chain
authentication
.SetNext(authorization)
.SetNext(validation);
// Valid request -- passes all handlers
var validRequest = new Request
{
AuthToken = "valid-token-123",
UserRole = "admin",
Body = "{ "action": "update" }"
};
Console.WriteLine("--- Processing valid request ---");
bool result = authentication.Handle(validRequest);
Console.WriteLine($"Result: {result}");
// Invalid request -- fails at authentication
var badTokenRequest = new Request
{
AuthToken = "expired-token",
UserRole = "admin",
Body = "{ "action": "delete" }"
};
Console.WriteLine("
--- Processing bad token request ---");
result = authentication.Handle(badTokenRequest);
Console.WriteLine($"Result: {result}");
The valid request passes through all three handlers. The bad token request gets rejected at the first handler, and neither authorization nor validation ever sees it -- unnecessary processing is avoided.
Chain of Responsibility in ASP.NET Core Middleware
If you've worked with ASP.NET Core, you've already used the chain of responsibility design pattern in C#. The middleware pipeline is a textbook implementation where each component decides whether to pass the request to the next or short-circuit.
The RequestDelegate type represents the "next handler" reference. Each middleware receives a RequestDelegate and decides whether to invoke it:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Logging middleware -- handle and continue
app.Use(async (context, next) =>
{
Console.WriteLine(
$"Request: {context.Request.Method} " +
$"{context.Request.Path}");
await next(context);
Console.WriteLine(
$"Response: {context.Response.StatusCode}");
});
// Auth middleware -- can short-circuit
app.Use(async (context, next) =>
{
if (!context.Request.Headers
.ContainsKey("Authorization"))
{
context.Response.StatusCode = 401;
await context.Response
.WriteAsync("Unauthorized");
return; // Short-circuit: don't call next
}
await next(context);
});
// Terminal middleware -- end of chain
app.Run(async context =>
{
await context.Response
.WriteAsync("Hello from the end of the chain!");
});
app.Run();
The logging middleware follows the "handle and continue" variant -- it does work before and after calling next. The auth middleware follows the "handle and decide" variant -- it either short-circuits with a 401 or passes to the next middleware. The terminal app.Run is the final handler that doesn't call next.
This is the chain of responsibility pattern in action. The RequestDelegate serves the same role as the _nextHandler reference in our earlier implementation. The middleware registration order determines the chain order, and each middleware is independent -- you can add, remove, or reorder middleware without touching the others.
Building Chains with Dependency Injection
In production applications, you'll want to construct handler chains through dependency injection rather than manually wiring them. This makes the chain configurable and testable, consistent with inversion of control principles.
First, let's define an interface-based handler contract that's more DI-friendly:
public interface IRequestHandler
{
IRequestHandler? NextHandler { get; set; }
bool Handle(Request request);
}
public abstract class BaseRequestHandler
: IRequestHandler
{
public IRequestHandler? NextHandler { get; set; }
public virtual bool Handle(Request request)
{
if (NextHandler is not null)
{
return NextHandler.Handle(request);
}
return true;
}
}
Next, register the handlers and build the chain using a factory method:
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
// Register individual handlers
services.AddTransient<AuthenticationHandler>();
services.AddTransient<AuthorizationHandler>(
sp => new AuthorizationHandler("admin"));
services.AddTransient<ValidationHandler>();
// Register chain factory
services.AddTransient<IRequestHandler>(sp =>
{
var auth = sp.GetRequiredService<AuthenticationHandler>();
var authz = sp.GetRequiredService<AuthorizationHandler>();
var val = sp.GetRequiredService<ValidationHandler>();
// Link the chain
auth.NextHandler = authz;
authz.NextHandler = val;
return auth;
});
var provider = services.BuildServiceProvider();
var chain = provider.GetRequiredService<IRequestHandler>();
var request = new Request
{
AuthToken = "valid-token-123",
UserRole = "admin",
Body = "{ "data": "test" }"
};
bool result = chain.Handle(request);
Console.WriteLine($"Chain result: {result}");
The factory lambda resolves each handler from the container, links them together, and returns the head of the chain. If you need to change the chain order or add a new handler, you modify the factory -- not the handlers themselves. The template method design pattern can also help if your handlers share a common processing skeleton but differ in specific steps.
When to Use the Chain of Responsibility Pattern
The chain of responsibility design pattern in C# fits specific scenarios well and should be avoided in others.
Good Fits
Request validation pipelines are the most natural use case. When an incoming request needs to pass through authentication, authorization, rate limiting, and input validation -- each as a separate concern -- the chain gives you clean separation and early-exit behavior.
Logging and audit chains work well when you want every handler to process the request and add its contribution. Each handler in the chain can log a different aspect -- request timing, payload content, response codes -- without any handler knowing about the others.
Event processing systems benefit from the chain when events need to be routed to the first handler that recognizes them. Input handling in UI frameworks often works this way: a key press event travels up the component hierarchy until a component handles it.
Poor Fits
When the processing order doesn't matter and every processor should handle every request, the observer design pattern is a better choice. Observers all receive the notification simultaneously rather than passing it along a sequential chain.
When you know exactly one handler should process the request and the selection criteria are known upfront, the strategy design pattern is simpler and more direct. Strategy selects the handler explicitly rather than iterating through a chain.
When you need to wrap the same interface with layered behavior rather than route a request through separate handlers, the decorator design pattern is the better fit. Decorators compose behavior by wrapping, while chains compose by forwarding.
Chain of Responsibility vs Other Patterns
Understanding how the chain of responsibility design pattern in C# relates to other behavioral patterns helps you make the right architectural choice.
Chain of Responsibility vs Command. The command design pattern encapsulates a request as an object so it can be stored, queued, or undone. The chain of responsibility routes a request through potential handlers. They solve different problems, but you can combine them -- command objects can flow through a chain of handlers that decide how to dispatch them.
Chain of Responsibility vs Decorator. Both involve linked objects delegating to the next. The decorator pattern wraps an object to add behavior while preserving the same interface, and every decorator always processes the request. The chain of responsibility gives each handler the choice to process, skip, or short-circuit.
Chain of Responsibility vs Observer. The observer pattern notifies all registered observers simultaneously. The chain of responsibility passes a request sequentially until a handler processes it. Use observer when all listeners should react. Use chain of responsibility when you need sequential processing with the ability to short-circuit.
Frequently Asked Questions
What is the chain of responsibility design pattern in C#?
The chain of responsibility design pattern in C# is a behavioral pattern that passes a request along a chain of handlers. Each handler examines the request and decides whether to process it, forward it to the next handler, or both. The sender only interacts with the first handler in the chain and doesn't know which handler ultimately processes the request.
How does ASP.NET Core middleware relate to the chain of responsibility pattern?
ASP.NET Core's middleware pipeline is a direct implementation of the chain of responsibility pattern. Each middleware receives a RequestDelegate for the next component and decides whether to invoke it. Middleware can do work before and after calling next, and it can short-circuit the pipeline by not calling next at all. The registration order determines the chain order, just as SetNext determines handler order in the classic implementation.
When should I use the chain of responsibility pattern instead of if-else chains?
Use the chain of responsibility pattern when your if-else chain is growing, when different conditions map to distinct responsibilities, or when you need the flexibility to add, remove, or reorder processing steps without modifying existing code. The chain distributes each condition into its own handler class, making the system more modular and testable.
Can handlers in the chain modify the request before passing it along?
Yes. Handlers can inspect, modify, enrich, or transform the request before forwarding it to the next handler. This is common in middleware pipelines where early handlers add context -- like authentication information or correlation IDs -- that downstream handlers use. The request object serves as shared state that flows through the chain, and each handler can contribute to it.
How do I handle the case where no handler processes the request?
You have several options. You can add a default handler at the end of the chain that catches unhandled requests and returns a fallback response. You can have the base handler's Handle method return a default result when the chain is exhausted. Or you can log unhandled requests for monitoring. The right approach depends on whether an unhandled request is an error in your domain or an acceptable outcome.
Is the chain of responsibility pattern thread-safe?
The pattern itself doesn't guarantee thread safety. If multiple threads send requests through the same chain simultaneously, you need to ensure that handlers don't share mutable state. Stateless handlers -- those that don't modify instance fields during processing -- are inherently thread-safe. For handlers with state, create a new chain per request or use thread-safe data structures.
How does the chain of responsibility pattern differ from the proxy pattern?
The proxy design pattern controls access to a single object by wrapping it with a surrogate. It's a one-to-one relationship. The chain of responsibility routes a request through multiple handlers in sequence. A proxy always delegates to its underlying object; a chain handler might stop the chain entirely.
Wrapping Up the Chain of Responsibility Design Pattern in C#
The chain of responsibility design pattern in C# is a powerful behavioral pattern for building flexible request-processing pipelines. Whether you're implementing custom validation chains, working with ASP.NET Core middleware, or building event-processing systems, the pattern gives you clean separation between handlers and the freedom to modify the pipeline without touching existing code.
Start by identifying places in your codebase where a request flows through multiple processing steps, especially where the steps might change independently or where you need the ability to short-circuit processing early. If you're building if-else chains that check conditions and route to different logic, that's a strong signal the chain of responsibility pattern can improve your design. Pair it with dependency injection to keep your chain assembly clean, and look at how ASP.NET Core middleware applies the same concept at the framework level.

