SoFunction
Updated on 2025-03-07

Detailed explanation of how to use the chain of responsibility mode in C#/.NET Core

Recently, a friend of mine was studying the classic "Gang Of Four" design pattern. He often comes to ask me what design patterns I use in actual business applications. Singleton mode, factory mode, intermediary mode - are all patterns I have used before and even wrote related articles. But there is one model that I have not written an article yet, namely the responsibility chain model.

What is the chain of responsibility? #

The chain of responsibility mode (I often called the chain of command mode before) is a pattern that allows "processing" objects in a hierarchical way. The classic definition in Wikipedia is

In object-oriented design, the chain of responsibility pattern is a design pattern composed of a command object source and a series of processing objects. Each processing object contains the logic of the command object it can handle, and the rest will be passed to the next processing object in the chain. Of course, there is also a mechanism to append new processing objects to the end of the chain here. Therefore the chain of responsibility is the object-oriented version of If..else if..else if...else...endif. The advantage is that the conditional operation blocks can be dynamically rearranged or configured at runtime.

Maybe you will feel that the above concept description is too abstract and difficult to understand, so let’s take a look at an example in real life.

Here we suppose we have a bank with three levels of employees in the bank, namely "teller", "supervisor", and "bank manager". If someone comes to withdraw money, the "teller" only allows withdrawal operations under $10,000. If the amount exceeds $10,000, its request will be passed to the "supervisor". The "Supervisor" can handle requests not exceeding $100,000, provided that the account must have an ID card. If there is no ID, the current request must be denied. If the withdrawal amount exceeds $100,000, the current request can be forwarded to the "Bank Manager" who can approve any withdrawal amount because if someone withdraws an amount exceeding $100,000, they are the VIP, and we don't care about the VIP ID and other regulations.

This is the hierarchical "chain" we discussed earlier, where everyone tries to process the current request and passes it to the next one if the request is not met. If we convert this scenario into code, it is what we call the chain of responsibility pattern. But before that, let's look at a bad implementation method.

A bad way to implement it#

Next, we first use If/Else block to solve the current problem.

class BankAccount
{
  bool idOnRecord { get; set; }

  void WithdrawMoney(decimal amount)
  {
    //Teller handling    if(amount < 10000)
    {
      ("Amount withdrawn by teller");
    } 
    // Supervisor handles    else if (amount < 100000)
    {
      if(!idOnRecord)
      {
        throw new Exception("The customer does not have an ID");
      }

      ("Amount withdrawn by the supervisor");
    }
    else
    {
      ("Amount withdrawn by the bank manager");
    }
  }
}

There are several problems with the above implementation method:

  • Adding a new employee level will be quite difficult because the IF/Else code block looks too messy
  • The logic of the "supervisor" checking ID ID is somewhat difficult to unit test because it must first pass other checks
  • Although now we only define the logic for withdrawal amounts, if we want to add other checks in the future (for example: VIP customers are always handled by supervisors), this logic will be difficult to manage and easily get out of control.

Using chain of responsibility mode coding

Let's rewrite some of this code below. Unlike before, here we create some "employee" objects that encapsulate their processing logic. The most important thing here is that we need to assign a direct superior to each employee object so that when they cannot process the current request, they can pass the request to the direct superior.

interface IBankEmployee
{
  IBankEmployee LineManager { get; }
  void HandleWithdrawRequest(BankAccount account, decimal amount);
}

class Teller : IBankEmployee
{
  public IBankEmployee LineManager { get; set; }

  public void HandleWithdrawRequest(BankAccount account, decimal amount)
  {
    if(amount > 10000)
    {
      (account, amount);
      return;
    }

    ("Amount withdrawn by teller");
  }
}

class Supervisor : IBankEmployee
{
  public IBankEmployee LineManager { get; set; }

  public void HandleWithdrawRequest(BankAccount account, decimal amount)
  {
    if (amount > 100000)
    {
      (account, amount);
      return;
    }

    if(!)
    {
      throw new Exception("The customer does not have an ID");
    }

    ("Amount withdrawn by the supervisor");
  }
}

class BankManager : IBankEmployee
{
  public IBankEmployee LineManager { get; set; }

  public void HandleWithdrawRequest(BankAccount account, decimal amount)
  {
    ("Amount withdrawn by the bank manager");
  }
}

We can create a chain of responsibility by specifying superiors. This looks a lot like an organization chart.

var bankManager = new BankManager();
var bankSupervisor = new Supervisor { LineManager = bankManager };
var frontLineStaff = new Teller { LineManager = bankSupervisor };

Here we can create a BankAccount class and convert the withdrawal method to be processed by the front desk employee.

class BankAccount
{
  public bool idOnRecord { get; set; }

  public void WithdrawMoney(IBankEmployee frontLineStaff, decimal amount)
  {
     (this, amount);
  }
}

Now, when we make withdrawal requests, the "teller" is always the first to handle it. If it cannot be processed, it will automatically send the request to the direct leader. The elegance of this pattern is as follows:

  1. The subsequent child in the chain does not need to know which child passed the command to it. Just like here, the "supervisor" does not need to know why the subordinate "teller" passes the request to him.
  2. "Teller" doesn't need to know the entire chain. He is only responsible for passing the request to the superior "supervisor" and expects the request to be processed at the superior "supervisor" (maybe further pass processing is required at present).
  3. When introducing new employee types, the entire organizational chart is easy to change. For example, I created a new "Teller Manager" role who can handle withdrawal requests between $10,000-50,000, and the direct superior of the "Teller Manager" is the "Supervisor." Here we do not need to do anything to deal with the "supervisor" object, we only need to change the direct superior of the "teller" to "teller manager".
  4. When writing unit tests, we can focus on only one employee role at a time. For example, when testing the "supervisor" logic, we do not need to test the logic of the "teller"

Extend our example#

Although I think the above examples can illustrate this pattern well, you usually find that some people will use a method called SetNext. Generally speaking, I think this is very rare in C# because we can use property getters and setters in C#. Using the SetVariableName method is usually a matter of the C++ era, which was usually the preferred method to encapsulate variables.

But the most important thing here is that other examples usually use abstract classes to enhance the way requests are passed. There is a problem in the previous code that many duplicate codes are written when passing the request to the next processor. So let's sort out the code.

The first thing we need to do here is to create an abstract class that allows us to process withdrawal requests in a standardized way. It should define a detection condition, if the condition is met, the withdrawal will be performed, otherwise the request will be passed to the direct superior. The modified code is as follows:

interface IBankEmployee
{
  IBankEmployee LineManager { get; }
  void HandleWithdrawRequest(BankAccount account, decimal amount);
}

abstract class BankEmployee : IBankEmployee
{
  public IBankEmployee LineManager { get; private set; }

  public void SetLineManager(IBankEmployee lineManager)
  {
     = lineManager;
  }

  public void HandleWithdrawRequest(BankAccount account, decimal amount)
  {
    if (CanHandleRequest(account, amount))
    {
      Withdraw(account, amount);
    } 
    else
    {
      (account, amount);
    }
  }

  abstract protected bool CanHandleRequest(BankAccount account, decimal amount);

  abstract protected void Withdraw(BankAccount account, decimal amount);
}

Next, we need to modify all employee classes to inherit from BankEmployee abstract class

class Teller : BankEmployee, IBankEmployee
{
  protected override bool CanHandleRequest(BankAccount account, decimal amount)
  {
    if (amount > 10000)
    {
      return false;
    }
    return true;
  }

  protected override void Withdraw(BankAccount account, decimal amount)
  {
    ("Amount withdrawn by teller");
  }
}

class Supervisor : BankEmployee, IBankEmployee
{
  protected override bool CanHandleRequest(BankAccount account, decimal amount)
  {
    if (amount > 100000)
    {
      return false;
    }
    return true;
  }

  protected override void Withdraw(BankAccount account, decimal amount)
  {
    if (!)
    {
      throw new Exception("The customer does not have an ID");
    }

    ("Amount withdrawn by the supervisor");
  }
}

class BankManager : BankEmployee, IBankEmployee
{
  protected override bool CanHandleRequest(BankAccount account, decimal amount)
  {
    return true;
  }

  protected override void Withdraw(BankAccount account, decimal amount)
  {
    ("Amount withdrawn by the bank manager");
  }
}

Please note here that in all scenarios, the HandleWithdrawRequest public method in the abstract class is called. This method will call the CanHandleRequest method defined in the subclass to detect whether the current role meets the conditions for processing the request. If so, the Withdraw method in the subclass will be called to handle the request, otherwise it will try to pass the request to the superior role.

We just need to change the way we create employee chains like the following code:

var bankManager = new BankManager();

var bankSupervisor = new Supervisor();
(bankManager);

var frontLineStaff = new Teller();
(bankSupervisor);

I need to reiterate here that I don't like using SetXXX, but I like it in many examples, so I added it in it.

In some examples, it is also placed in an abstract class to determine whether the employee meets the conditions for processing the request. I personally don't like to do this because it means that all handlers have to use similar logic. For example, all checks are currently based on the withdrawal amount, but if we want to implement a special handler whose conditions are related to the VIP flag, we will have to reuse IF/Else in the abstract class again, which in turn brings us back to the IF/Else hell.

When should I use the chain of responsibility model? #

The best use scenario for this model is that you have a logical processing chain on your business, which must be run in sequence each time. Note here that chain forking is a variation of this pattern, but it will be very complicated to process quickly. So when I model the "command chain" scenario in the real world, I usually use this pattern. This is why I take the bank as an example, because it is the "chain of responsibility" that can be modeled with code in the real world.

This is the article about how to use the responsibility chain model in C#/.NET Core. For more related content in C#/.NET Core, please search for my previous articles or continue browsing the related articles below. I hope you will support me in the future!

original:Chain Of Responsbility Pattern In C#/.NET Core
Author: Wade
Translator: Lamond Lu
Source:/lwqlun/p/

Copyright: This article is licensed under the "Attribution 4.0 International" Creative Commons License.