Skip to content
Type something to search...
Chain of Responsibility Design Pattern in C#: Passing the Buck, One Object at a Time

Chain of Responsibility Design Pattern in C#: Passing the Buck, One Object at a Time

Have you ever faced a situation where handling requests feels like a chaotic game of hot potato? You throw a request from one object to another, hoping someone—anyone—will eventually handle it. Sounds messy, doesn’t it?

What if I told you there’s a cleaner, smarter, and more organized way to handle this? Enter the Chain of Responsibility pattern, your go-to design pattern to tame unruly, cascading requests. Think of it like passing the baton in a relay race—smooth, coordinated, and efficient.

Let’s dive deep into the world of Chain of Responsibility, focusing exclusively on software architects using Microsoft technologies, specifically in C#. Buckle up, because by the end of this, you’ll not only understand the pattern—you’ll master it!


What’s the Chain of Responsibility Pattern Anyway?

Picture this scenario: you have an incoming request. Maybe it’s a logging request, a user authorization request, or a purchase approval. Your system has multiple handlers, but you’re unsure which one will ultimately handle it. What do you do?

Here’s where the Chain of Responsibility (CoR) pattern shines.

The Chain of Responsibility pattern allows you to pass a request along a chain of handlers. Each handler decides either to handle the request or pass it along to the next one.

Think of it like a corporate chain of command. If a junior manager can’t handle a request, she escalates it to a senior manager, and if it’s still unresolved, it moves up to the VP, then finally to the CEO.


Core Principles of the Chain of Responsibility Pattern

Understanding the principles is key to mastering the pattern. Let’s break it down:

1. Decoupling Sender and Receiver

The sender doesn’t have to know which handler will take care of the request. This creates a loose coupling and flexible system design.

2. Single Responsibility Principle (SRP)

Each handler has a clearly defined single responsibility. It either handles the request or passes it on. Clean, clear, concise.

3. Open/Closed Principle (OCP)

The system should be open to extension (adding new handlers) but closed to modification (no need to change existing handlers).


When Should You Use the Chain of Responsibility Pattern?

Not every nail needs a hammer, right? Similarly, CoR is powerful, but it fits best in specific scenarios. Ask yourself these questions to know if CoR is your answer:

  • Do multiple objects have the potential to handle a request?
  • Do you want to avoid hardcoding the decision-making logic into your client code?
  • Is your handling logic subject to change often, requiring flexibility?

If you said “yes” to any of these, you’re in the right place.

Common use cases include:

  • Logging systems
  • Authorization and authentication handlers
  • GUI event handling
  • Approval workflows (e.g., expense approvals, leave requests)
  • Exception handling and validation chains

Key Components of the Chain of Responsibility Pattern

Let’s clarify some jargon:

  • Handler Interface: Defines a method to handle requests and optionally sets the next handler.
  • Concrete Handlers: Implements the Handler interface. Decides if they can handle the request or pass it along.
  • Client: The object sending the request to the chain.

Quick analogy:

Imagine a delivery chain. Your package moves from the warehouse to the shipping center, then to your local delivery hub, and finally to your door. If a link can’t deliver (say, due to weather issues), the next step handles the fallback. The CoR pattern is exactly this, but for code.


Time to Code: Implementing Chain of Responsibility in C# (With Super-Detailed Example!)

Now, let’s see it in action. We’ll create a simple but comprehensive example:

Scenario: Expense Approval System

Imagine you have a system where employees submit expenses for approval. Expenses below $1000 can be approved by a supervisor. Anything between $1000 and $5000 goes to a manager, and anything above that must be approved by the director.


Step 1: Define the Handler Interface

// The Handler interface
public abstract class Approver
{
    protected Approver successor;

    public void SetSuccessor(Approver successor)
    {
        this.successor = successor;
    }

    public abstract void ProcessRequest(Expense expense);
}

Step 2: Define the Request (Expense)

// Expense class representing the request
public class Expense
{
    public int Amount { get; }
    public string Purpose { get; }

    public Expense(int amount, string purpose)
    {
        Amount = amount;
        Purpose = purpose;
    }
}

Step 3: Create Concrete Handlers

Supervisor Handler

// Supervisor class
public class Supervisor : Approver
{
    public override void ProcessRequest(Expense expense)
    {
        if (expense.Amount < 1000)
        {
            Console.WriteLine($"Supervisor approved ${expense.Amount} for {expense.Purpose}");
        }
        else if (successor != null)
        {
            successor.ProcessRequest(expense);
        }
    }
}

Manager Handler

// Manager class
public class Manager : Approver
{
    public override void ProcessRequest(Expense expense)
    {
        if (expense.Amount < 5000)
        {
            Console.WriteLine($"Manager approved ${expense.Amount} for {expense.Purpose}");
        }
        else if (successor != null)
        {
            successor.ProcessRequest(expense);
        }
    }
}

Director Handler

// Director class
public class Director : Approver
{
    public override void ProcessRequest(Expense expense)
    {
        if (expense.Amount >= 5000)
        {
            Console.WriteLine($"Director approved ${expense.Amount} for {expense.Purpose}");
        }
    }
}

Step 4: Client Code (Using Our Chain)

Let’s put it all together:

class Program
{
    static void Main()
    {
        // Create approvers
        Approver supervisor = new Supervisor();
        Approver manager = new Manager();
        Approver director = new Director();

        // Link them into a chain
        supervisor.SetSuccessor(manager);
        manager.SetSuccessor(director);

        // Let's submit some expenses and watch the magic happen!
        Expense expense1 = new Expense(500, "Team Lunch");
        Expense expense2 = new Expense(2500, "Conference Fees");
        Expense expense3 = new Expense(7500, "New Office Furniture");

        supervisor.ProcessRequest(expense1);
        supervisor.ProcessRequest(expense2);
        supervisor.ProcessRequest(expense3);

        Console.ReadKey();
    }
}

Output:

Supervisor approved $500 for Team Lunch
Manager approved $2500 for Conference Fees
Director approved $7500 for New Office Furniture

See how clean that is? Each handler knows exactly when to step in. The supervisor handles the small stuff, the manager takes mid-range requests, and the director jumps in only for the big decisions.

Now, imagine trying to manage this with a bunch of if-else conditions inside your main program. Not pretty, right? This is exactly why the Chain of Responsibility pattern is your best friend for keeping code clean and maintainable!


Let’s Go Deeper: Analyzing the Components Clearly

You’ve seen the pattern at work, but let’s dive deeper into what just happened, analyzing each component:

1. Approver (Handler Interface)

  • Defines an interface or abstract class for handling requests and chaining handlers together.
  • Holds a reference to the next handler.
  • Implements a method to pass the request along if it can’t handle it itself.

2. Concrete Handlers (Supervisor, Manager, Director)

  • Each concrete handler decides if it can handle the incoming request.
  • If it can’t, it forwards the request to the next handler in the chain.

3. Expense (Request)

  • Encapsulates details of the request that handlers use to decide whether they should handle or pass along.

4. Client (Program class)

  • Sets up the chain once.
  • Sends requests into the chain without knowing who exactly will handle them.

Different Ways to Implement Chain of Responsibility in C#

Sure, the traditional approach we explored earlier is great, but C# is versatile, and you have multiple elegant ways to build your chain. Let’s see a couple of these alternatives:

Method 1: Using Delegates (Functional CoR)

Delegates provide an elegant functional-style approach to Chain of Responsibility. It’s clean, concise, and fits nicely with modern C#:

Example:

// Functional-style Chain using Delegates
public delegate bool ApprovalHandler(Expense expense);

public class ExpenseProcessor
{
    private readonly ApprovalHandler chain;

    public ExpenseProcessor()
    {
        chain = ApproveBySupervisor;
        chain += ApproveByManager;
        chain += ApproveByDirector;
    }

    public void ProcessExpense(Expense expense)
    {
        foreach (ApprovalHandler handler in chain.GetInvocationList())
        {
            if (handler(expense))
                break;
        }
    }

    private bool ApproveBySupervisor(Expense expense)
    {
        if (expense.Amount < 1000)
        {
            Console.WriteLine($"Supervisor approved ${expense.Amount}");
            return true;
        }
        return false;
    }

    private bool ApproveByManager(Expense expense)
    {
        if (expense.Amount < 5000)
        {
            Console.WriteLine($"Manager approved ${expense.Amount}");
            return true;
        }
        return false;
    }

    private bool ApproveByDirector(Expense expense)
    {
        Console.WriteLine($"Director approved ${expense.Amount}");
        return true;
    }
}

Why Delegates?
Think of delegates as creating a dynamic assembly line. Each function checks the request, and the first capable handler deals with it immediately.


Method 2: Using Events for Loosely Coupled Chains

You can leverage C# events for an even looser coupling:

Example:

public class ExpenseEventArgs : EventArgs
{
    public Expense Expense { get; set; }
    public bool Handled { get; set; }
}

public class ExpenseHandler
{
    public event EventHandler<ExpenseEventArgs> ApproveExpense;

    public void SubmitExpense(Expense expense)
    {
        var args = new ExpenseEventArgs { Expense = expense };
        ApproveExpense?.Invoke(this, args);

        if (!args.Handled)
            Console.WriteLine("Expense not approved by anyone!");
    }
}

// Subscribe handlers
ExpenseHandler handler = new ExpenseHandler();

handler.ApproveExpense += (sender, args) =>
{
    if (args.Expense.Amount < 1000)
    {
        Console.WriteLine($"Supervisor approved ${args.Expense.Amount}");
        args.Handled = true;
    }
};

handler.ApproveExpense += (sender, args) =>
{
    if (!args.Handled && args.Expense.Amount < 5000)
    {
        Console.WriteLine($"Manager approved ${args.Expense.Amount}");
        args.Handled = true;
    }
};

handler.ApproveExpense += (sender, args) =>
{
    if (!args.Handled)
    {
        Console.WriteLine($"Director approved ${args.Expense.Amount}");
        args.Handled = true;
    }
};

Events let you plug handlers in and out easily—like connecting and disconnecting wires.


Practical Use Cases of the Chain of Responsibility Pattern

Wondering where exactly this shiny tool fits best? Let’s simplify:

  • Event Handling (UI): Handling UI events where clicks or keyboard inputs move from one element to another.
  • Logging Frameworks: Each logger passes messages it can’t handle up to the next level.
  • Middleware in Web APIs: Handling authentication, caching, and logging in ASP.NET Core middleware.
  • Validation Chains: Sequentially validating user inputs.
  • Error Handling: Catching errors progressively (e.g., first locally, then globally).

Anti-Patterns to Avoid with Chain of Responsibility

Great patterns can be misused too! Watch out for these traps:

1. Chain Too Long or Complex

  • A chain shouldn’t become the proverbial bureaucracy—slow and difficult to trace.
  • Keep chains concise and focused.

2. Unhandled Requests

  • Always have a fallback or default handler.
  • Otherwise, your request might vanish into a black hole. (Who wants that?)

3. Misuse for Simple Conditions

  • For trivial conditional logic, if-else might actually be clearer.
  • Don’t use CoR to solve what can be done with a straightforward condition.

Advantages of Using the Chain of Responsibility Pattern

Here’s why developers love it:

  • Decoupling: Sender and receiver stay independent and flexible.
  • Flexibility: Easy to add, remove, or reorder handlers.
  • Open-Closed Compliance: Extend functionality without modifying existing code.
  • Single Responsibility Principle: Handlers perform focused, easily maintainable tasks.
  • Simplified Object Interactions: Reduces complexity by avoiding a tangled web of relationships.

Disadvantages of Chain of Responsibility Pattern

It’s not all roses, though. Watch out for:

  • Request Overhead: If handlers are many, performance might degrade.
  • Debugging Complexity: Harder to track down exactly who handles what if chains grow overly complex.
  • Runtime Behavior: You can’t easily predict at compile time who handles what.

Anti-Patterns to Avoid (A Quick Recap & Reminder!)

(Yes, this deserves emphasizing again!)

  • Avoid overly complicated chains.
  • Avoid leaving requests unhandled without notification.
  • Avoid using CoR for scenarios that simple conditions can handle better.

Conclusion: Is the Chain of Responsibility Pattern Worth It?

Absolutely, yes—but use it wisely!

The Chain of Responsibility pattern is a powerful tool that helps architects and developers craft elegant, maintainable, and robust systems. It gives you clear paths for handling complex scenarios while keeping your codebase clean.

Just remember:

  • Keep your chains simple, concise, and focused.
  • Choose the right implementation style (traditional, delegates, or events) based on your use case.
  • Always consider potential overhead and debugging complexity.

Think of Chain of Responsibility as a relay team: efficient and coordinated, each member doing just enough before gracefully passing responsibility onwards. When implemented thoughtfully, this pattern is your friend—helping you build scalable, flexible systems that make sense.

Now go forth and chain those responsibilities—like a boss!

Related Posts

Mastering the Command Design Pattern in C#: A Fun and Practical Guide for Software Architects

Mastering the Command Design Pattern in C#: A Fun and Practical Guide for Software Architects

📚 Introduction Hey there, software architect! Have you ever felt like you're constantly juggling flaming torches when managing requests in a large application? You're adding commands here, remo

Read More
Interpreter Design Pattern Explained: A Deep Dive for C# Developers (With Real-World Examples)

Interpreter Design Pattern Explained: A Deep Dive for C# Developers (With Real-World Examples)

Ever felt like explaining things to a machine is just too tough? Ever wished you could give instructions in a more human-readable way without getting tangled up in complex code logic? Well, my friend,

Read More
Iterator Design Pattern: The Ultimate Guide for Software Architects Using Microsoft Technologies

Iterator Design Pattern: The Ultimate Guide for Software Architects Using Microsoft Technologies

So, you're here because you've heard whispers about this mysterious thing called the Iterator Pattern. Or maybe you're a seasoned developer who's looking for a comprehensive refresher filled with

Read More
Mastering the Mediator Design Pattern in C#: Your Secret Weapon for Cleaner, Smarter, Microsoft-Based Software Architectures

Mastering the Mediator Design Pattern in C#: Your Secret Weapon for Cleaner, Smarter, Microsoft-Based Software Architectures

Ever felt like you're at a noisy party where everyone's talking over each other? You know, the kind of chaos where communication breaks down and no one really understands what's going on? Well, softwa

Read More
The Memento Design Pattern: Saving Your Objects' State (Without Losing Your Mind)

The Memento Design Pattern: Saving Your Objects' State (Without Losing Your Mind)

Ever found yourself wishing you had a "save" button in real life? Maybe you accidentally deleted a chunk of code, overwrote some critical data, or perhaps your latest code refactor went terribly wrong

Read More
Observer Design Pattern Explained: A Deep Dive with C# (That Actually Makes Sense!)

Observer Design Pattern Explained: A Deep Dive with C# (That Actually Makes Sense!)

Introduction Ever felt like you're juggling way too many balls at once in your software application? Changes in one part of the codebase cascading into chaos elsewhere—sound familiar? If so, buc

Read More