Chain of Responsibility Design Pattern in .NET Core API
A practical Chain of Responsibility guide in C# with Pure and Cascading chain variants for clean request pipelines.

Intro
As API workflows grow, one request may require validation, enrichment, authorization, and business checks in sequence. The Chain of Responsibility pattern makes this clean by passing the same request through linked handlers.
What Is the Chain of Responsibility Pattern?
Chain of Responsibility is a behavioral design pattern where multiple handlers can process a request one after another. Each handler decides whether to handle, forward, or stop the request.
This removes large conditional blocks from callers and makes request processing pipelines extensible.
Chain of Responsibility Class Example
The example below models a request pipeline for order processing.
public sealed record OrderRequest(Guid CustomerId, decimal Amount, bool IsPriority);
public interface IOrderHandler{ IOrderHandler SetNext(IOrderHandler next); Task HandleAsync(OrderRequest request, CancellationToken cancellationToken);}
public abstract class OrderHandlerBase : IOrderHandler{ private IOrderHandler? _next;
public IOrderHandler SetNext(IOrderHandler next) { _next = next; return next; }
public virtual async Task HandleAsync(OrderRequest request, CancellationToken cancellationToken) { if (_next is not null) await _next.HandleAsync(request, cancellationToken); }}
public sealed class ValidationHandler : OrderHandlerBase{ public override async Task HandleAsync(OrderRequest request, CancellationToken cancellationToken) { if (request.Amount <= 0) throw new InvalidOperationException("Order amount must be greater than zero.");
await base.HandleAsync(request, cancellationToken); }}
public sealed class PriorityRoutingHandler : OrderHandlerBase{ public override async Task HandleAsync(OrderRequest request, CancellationToken cancellationToken) { if (request.IsPriority) Console.WriteLine("Priority queue selected.");
await base.HandleAsync(request, cancellationToken); }}Why This Works
- Decoupled flow: Caller sends one request to the head handler.
- Composable steps: Add or reorder handlers without touching callers.
- Focused responsibilities: Each handler owns one concern.
When to Use It
- building request or validation pipelines
- implementing layered business rule checks
- applying middleware-style processing in services
- avoiding large
if/elseblocks in application services
If only one fixed operation exists, this pattern can be unnecessary.
Important Note for ASP.NET Core
In ASP.NET Core, Chain of Responsibility maps naturally to middleware and service pipelines. You can assemble handlers in a composition root and keep controllers minimal.
builder.Services.AddScoped<ValidationHandler>();builder.Services.AddScoped<PriorityRoutingHandler>();A coordinator service can resolve handlers from DI and chain them in order.
Core Components of the Pattern
| Part | Purpose | Example in this article |
|---|---|---|
| Handler interface | Defines request handling contract | IOrderHandler |
| Base handler | Stores and forwards to next handler | OrderHandlerBase |
| Concrete handlers | Apply specific processing logic | ValidationHandler, PriorityRoutingHandler |
| Client | Sends request to first handler | API service/controller |
Common Chain of Responsibility Variations
Pure Chain (single handler)
A request is handled by exactly one handler. Once handled, forwarding stops.
public sealed class DiscountResolver : OrderHandlerBase{ public override async Task HandleAsync(OrderRequest request, CancellationToken cancellationToken) { if (request.Amount > 1000) { Console.WriteLine("Enterprise discount applied."); return; }
await base.HandleAsync(request, cancellationToken); }}Cascading Chain (multiple handlers)
Every handler can act and then pass the request forward.
public sealed class AuditHandler : OrderHandlerBase{ public override async Task HandleAsync(OrderRequest request, CancellationToken cancellationToken) { Console.WriteLine("Audit recorded."); await base.HandleAsync(request, cancellationToken); }}How the Variations Differ
| Variation | Execution model | Best use case |
|---|---|---|
| Pure Chain | First capable handler completes request | Routing to one destination handler |
| Cascading Chain | Multiple handlers process the same request | Validation + enrichment + logging pipelines |
SOLID Principles Behind It
SRP - Single Responsibility Principle
Each handler encapsulates one processing step.
OCP - Open/Closed Principle
You extend behavior by adding handlers, not by rewriting existing ones.
DIP - Dependency Inversion Principle
Callers depend on handler abstractions, not concrete classes.
Advantages and Disadvantages
Advantages
- keeps request pipelines modular
- improves maintainability by isolating rules
- allows flexible handler ordering
Disadvantages (and Optional Alternatives)
- debugging deep chains can be harder without tracing Optional pattern: Template Method (when sequence is fixed and stable).
- incorrect chain order can cause subtle behavior issues Optional pattern: Strategy (when only one algorithm should run).
UML Diagram
classDiagram class Client class IOrderHandler { <<interface>> +SetNext(next) +HandleAsync(request, token) } class OrderHandlerBase class ValidationHandler class PriorityRoutingHandler
Client --> IOrderHandler OrderHandlerBase ..|> IOrderHandler ValidationHandler --|> OrderHandlerBase PriorityRoutingHandler --|> OrderHandlerBase OrderHandlerBase --> IOrderHandler : nextA Quick Usage Example
var validation = new ValidationHandler();var routing = new PriorityRoutingHandler();
validation.SetNext(routing);
await validation.HandleAsync(new OrderRequest(Guid.NewGuid(), 240, true), CancellationToken.None);How to Validate the Pattern
Use these checks to verify your implementation:
- handler classes should depend on abstractions, not caller internals
- chain order should be test-covered for expected outcomes
- pure chains should stop once a request is handled
- cascading chains should preserve handler execution order
- each handler should be unit testable in isolation
Summary
The Chain of Responsibility pattern helps .NET APIs process requests through flexible handler pipelines. With Pure and Cascading variants, you can choose between single-handler routing and full multi-step processing while keeping callers decoupled.




