Chain of Responsibility Pattern in C#: Definition, Use Cases, Pros, Cons, and Examples
The Chain of Responsibility pattern in C# is a behavioral design pattern that passes a request through a chain of handler objects until one of them processes the request.
The Chain of Responsibility pattern allows multiple objects to handle a request without tightly coupling the sender to a specific receiver. In this pattern, each handler decides either to process the request or pass it to the next handler in the chain. This creates a flexible processing pipeline where responsibilities are distributed across multiple classes. The pattern is commonly implemented using abstract handler classes or interfaces that contain a reference to the next handler. In C#, it is widely used in middleware pipelines, event processing systems, validation frameworks, and logging systems.
Why We Use Chain of Responsibility Pattern in C#?
We use the Chain of Responsibility pattern in C# to:
• Reduce tight coupling between request senders and receivers
• Create flexible and extendable processing pipelines
• Allow multiple objects to participate in request handling
• Dynamically add or remove handlers
• Follow the Single Responsibility Principle
• Simplify conditional processing logic
• Improve maintainability in large applications
When Should We Use Chain of Responsibility Pattern in C#?
The Chain of Responsibility pattern should be used when:
• More than one object may handle a request
• The exact handler is unknown beforehand
• Requests need sequential processing
• You want to avoid large if-else or switch statements
• Processing logic should be dynamically configurable
• Different validation or filtering steps are required
• Request processing should be decoupled from business logic
Common Programming Problems Solved by Chain of Responsibility
• Validation pipelines
• Authentication and authorization systems
• Logging frameworks
• Exception handling pipelines
• Request filtering systems
• Event handling systems
• Approval workflows
• Middleware architectures
• Command processing pipelines
Structure of Chain of Responsibility Pattern in C#
Typical participants:
• Client
• Handler Interface / Abstract Handler
• Concrete Handlers
• Request Object
Basic workflow:
• Client sends request to first handler
• Handler processes or forwards request
• Next handler repeats process
• Chain continues until handled or chain ends
Chain of Responsibility Pattern Examples in C#
Example 1: Logger System
Different log levels should be handled by different loggers.
using System;
public abstract class Logger
{
protected Logger nextLogger;
public void SetNext(Logger logger)
{
nextLogger = logger;
}
public void LogMessage(string level, string message)
{
if (CanHandle(level))
{
Write(message);
}
else if (nextLogger != null)
{
nextLogger.LogMessage(level, message);
}
}
protected abstract bool CanHandle(string level);
protected abstract void Write(string message);
}
public class InfoLogger : Logger
{
protected override bool CanHandle(string level)
{
return level == "INFO";
}
protected override void Write(string message)
{
Console.WriteLine("INFO: " + message);
}
}
public class ErrorLogger : Logger
{
protected override bool CanHandle(string level)
{
return level == "ERROR";
}
protected override void Write(string message)
{
Console.WriteLine("ERROR: " + message);
}
}
public class Program
{
public static void Main()
{
Logger infoLogger = new InfoLogger();
Logger errorLogger = new ErrorLogger();
infoLogger.SetNext(errorLogger);
infoLogger.LogMessage("INFO", "Application started");
infoLogger.LogMessage("ERROR", "Something failed");
}
}
Example 2: Authentication Middleware
Requests should pass through authentication and authorization steps.
using System;
public abstract class Middleware
{
protected Middleware next;
public void SetNext(Middleware middleware)
{
next = middleware;
}
public virtual bool Handle(string user)
{
if (next != null)
{
return next.Handle(user);
}
return true;
}
}
public class AuthenticationMiddleware : Middleware
{
public override bool Handle(string user)
{
if (string.IsNullOrEmpty(user))
{
Console.WriteLine("Authentication failed");
return false;
}
Console.WriteLine("Authentication successful");
return base.Handle(user);
}
}
public class AuthorizationMiddleware : Middleware
{
public override bool Handle(string user)
{
if (user != "admin")
{
Console.WriteLine("Authorization failed");
return false;
}
Console.WriteLine("Authorization successful");
return base.Handle(user);
}
}
public class Program
{
public static void Main()
{
Middleware auth = new AuthenticationMiddleware();
Middleware authorization = new AuthorizationMiddleware();
auth.SetNext(authorization);
auth.Handle("admin");
}
}
Example 3: Expense Approval System
Expense requests should be approved by managers with different approval limits.
using System;
public class ExpenseRequest
{
public decimal Amount { get; set; }
public ExpenseRequest(decimal amount)
{
Amount = amount;
}
}
public abstract class Approver
{
protected Approver nextApprover;
public void SetNext(Approver approver)
{
nextApprover = approver;
}
public abstract void Approve(ExpenseRequest request);
}
public class TeamLead : Approver
{
public override void Approve(ExpenseRequest request)
{
if (request.Amount <= 1000)
{
Console.WriteLine("TeamLead approved request");
}
else if (nextApprover != null)
{
nextApprover.Approve(request);
}
}
}
public class Manager : Approver
{
public override void Approve(ExpenseRequest request)
{
if (request.Amount <= 5000)
{
Console.WriteLine("Manager approved request");
}
else if (nextApprover != null)
{
nextApprover.Approve(request);
}
}
}
public class Director : Approver
{
public override void Approve(ExpenseRequest request)
{
if (request.Amount > 5000)
{
Console.WriteLine("Director approved request");
}
}
}
public class Program
{
public static void Main()
{
Approver teamLead = new TeamLead();
Approver manager = new Manager();
Approver director = new Director();
teamLead.SetNext(manager);
manager.SetNext(director);
teamLead.Approve(new ExpenseRequest(7000));
}
}
Most Known Real-World Use Cases in C#
• ASP.NET Core middleware pipeline
• Request validation frameworks
• Logging frameworks
• Authentication and authorization systems
• Exception handling pipelines
• Approval workflows
• Event bubbling systems in UI frameworks
Advantages of Chain of Responsibility Pattern in C#
• Reduces coupling between sender and receiver
• Improves code flexibility
• Easier to extend processing steps
• Supports Open/Closed Principle
• Simplifies complex conditional logic
• Enables dynamic handler ordering
• Encourages reusable handler components
• Improves maintainability
Disadvantages (Weak Points) of Chain of Responsibility Pattern in C#
• Request may remain unhandled
• Debugging chains can become difficult
• Long chains may reduce performance
• Handler order can create unexpected behavior
• Increases number of classes
• Runtime configuration may become complex
• Harder to understand for beginners
Comparison with Similar Patterns
| Pattern | Main Purpose | Request Flow | Coupling Level | Typical Usage | Main Difference from Chain of Responsibility |
|---|---|---|---|---|---|
| Chain of Responsibility | Pass request through handlers | Sequential chain | Loose | Middleware, validation, logging | Handlers decide whether to process or forward request |
| Command | Encapsulate requests as objects | Direct execution | Loose | Undo/redo, task queues | Focuses on request encapsulation rather than handler chains |
| Mediator | Centralize communication | Through mediator | Very loose | UI communication, messaging systems | Uses a central coordinator instead of sequential handlers |
| Strategy | Select algorithm dynamically | Single strategy | Loose | Sorting, payment processing | Selects one behavior instead of passing through handlers |
| Observer | Notify multiple subscribers | Broadcast | Loose | Events, notifications | All observers receive notifications instead of sequential processing |
Simplified UML-Style Structure
| Component | Responsibility |
|---|---|
| Client | Sends request to first handler |
| Handler | Defines interface for handling requests |
| ConcreteHandler | Processes request or forwards it |
| Next Handler | Receives forwarded request |