Manikandan — Manikandan
Updated on 4 min read Manikandan Design Patterns

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.

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/else blocks 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

PartPurposeExample in this article
Handler interfaceDefines request handling contractIOrderHandler
Base handlerStores and forwards to next handlerOrderHandlerBase
Concrete handlersApply specific processing logicValidationHandler, PriorityRoutingHandler
ClientSends request to first handlerAPI 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

VariationExecution modelBest use case
Pure ChainFirst capable handler completes requestRouting to one destination handler
Cascading ChainMultiple handlers process the same requestValidation + 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 : next

A 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.

Share:
Back to Blog

Related Posts

View All Posts »