Chain of Responsibility Pattern in C#: Definition, Use Cases, Pros, Cons, and Examples

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