Intermediate

C-Sharp Design Patterns Chain of Responsibility

This course covers the Chain of Responsibility design pattern in C#. It is intended for those who want to understand how this pattern works and know how to implement it in their C# applic...

Table of Contents

  1. Course Overview
  2. Introduction to the Chain of Responsibility pattern
  1. Demo 1 — First look at the Chain of Responsibility (user validation)
  1. Demo 2 — Payment Processing
  1. Demo 3 — Improvement of the Payment Processor (Handler/Receiver separation)
  1. Demo 4 — ILogger: existing implementation in .NET
  2. Demo 5 — ASP.NET Core Processing Pipeline (middlewares)
  3. Boss’s benefits and compromises
  4. Summary and conclusion

1. Course Overview

This course covers the Chain of Responsibility design pattern in C#. It is intended for those who want to understand how this pattern works and know how to implement it in their C# applications.

What you will learn

  • What the Chain of Responsibility pattern is and its characteristics.
  • The benefits and trade-offs of its use.
  • How to implement the pattern in new and existing solutions.
  • How to identify and exploit existing implementations of this pattern.

Prerequisites

  • Be familiar with C# syntax and know how to compile and run .NET applications.
  • No other prior knowledge is necessary.

2. Introduction to the Chain of Responsibility pattern

2.1 Course objectives

The main objective is to understand how to implement the Chain of Responsibility pattern in your applications. You will then be able to identify and exploit existing implementations of this pattern in different types of applications.

This pattern is a behavioral design pattern that will drastically change the way you build software.

2.2 Pattern characteristics

The Chain of Responsibility pattern is commonly identified by three distinct elements:

ElementRole
Sender (transmitter)Invokes something in the application
Handler (manager)Iterates through a chain of receivers
Receiver (receiver)Can process the given order

2.3 General operation

Conceptual example with a logger:

  • The sender (anything in the application) calls Logger.Log.
  • This invokes the handler.
  • The handler has a list of concrete implementations of loggers (the receivers).
  • The pattern allows you to execute one or more different handlers added to the chain.
  • Receivers can be: a ConsoleLogger, a FileLogger, a DatabaseLogger.

Basic rules:

  1. The receiver contains the logic to decide whether to act on the request or not.
  2. The handler ensures that each receiver receives the request.
  3. Even if the request is not processed by a particular receiver, it is still forwarded to the next one in the chain.
  4. The sender does not have to know the concrete implementations — it can extend the string easily.

The request object contains all the information the handlers need to execute (for example, the message and severity for a logger).

The Chain of Responsibility pattern is a very powerful, but relatively simple, way to decouple an application to make it more extensible and maintainable.


3. Demo 1 — First look at Chain of Responsibility (user validation)

3.1 Starting point: the problem to be solved

The starting application is a simple console application which allows you to register a user in a system. The User model contains:

  • A name
  • A social security number
  • The region of citizenship
  • Date of birth

User model:

public class User
{
    public string Name { get; }
    public string SocialSecurityNumber { get; }
    public DateTimeOffset DateOfBirth { get; }
    public RegionInfo CitizenshipRegion { get; }

    // Calcul de l'âge approximatif
    public int Age => (int)((DateTimeOffset.UtcNow - DateOfBirth.UtcDateTime).TotalDays / 365.2425);

    public User(string name, 
        string socialSecurityNumber,
        RegionInfo citizenshipRegion,
        DateTimeOffset dateOfBirth)
    {
        Name = name;
        SocialSecurityNumber = socialSecurityNumber;
        DateOfBirth = dateOfBirth;
        CitizenshipRegion = citizenshipRegion;
    }
}

Initial problem — UserProcessor with if/else if/else:

The initial UserProcessor uses a series of if/else if/else blocks to validate the user. This approach is fragile and difficult to maintain:

public class UserProcessor
{
    private SocialSecurityNumberValidator socialSecurityNumberValidator
        = new SocialSecurityNumberValidator();

    public bool Register(User user)
    {
        if (!socialSecurityNumberValidator.Validate(user.SocialSecurityNumber, user.CitizenshipRegion))
        {
            return false;
        }
        else if (user.Age < 18)
        {
            return false;
        }
        else if (user.Name.Length <= 1)
        {
            return false;
        }
        else if (user.CitizenshipRegion.TwoLetterISORegionName == "NO")
        {
            return false;
        }
        else
        {
            return true;
        }
    }
}

The validation rules are:

  1. Social security number must be valid (depends on region/country).
  2. User must be 18 years or older.
  3. The name must be more than one character.
  4. The system does not support Norwegian citizens (“NO” region).

The Chain of Responsibility pattern is an object-oriented way of replacing the if/else if/else idiom with something more structured.

Social Security Number Validator:

public class SocialSecurityNumberValidator
{
    public bool Validate(string socialSecurityNumber, RegionInfo region)
    {
        switch (region.TwoLetterISORegionName)
        {
            case "SE": return ValidateSwedishSocialSecurityNumber(socialSecurityNumber);
            case "US": return ValidateUnitedStatesSocialSecurityNumber(socialSecurityNumber);
            default: throw new UnsupportedSocialSecurityNumberException();
        }
    }

    private bool ValidateSwedishSocialSecurityNumber(string socialSecurityNumber)
    {
        return socialSecurityNumber.Length > 1;
    }

    private bool ValidateUnitedStatesSocialSecurityNumber(string socialSecurityNumber)
    {
        return socialSecurityNumber.Length > 2;
    }
}

3.2 The IHandler interface and the Handler abstract class

The first step is to create the pattern infrastructure. We define a generic interface IHandler<T>:

public interface IHandler<T> where T : class
{
    IHandler<T> SetNext(IHandler<T> next);
    void Handle(T request);
}

This interface describes that:

  • Handlers can be chained with SetNext.
  • You can process a request with Handle.

Next, we create the Handler abstract class which provides the basic implementation:

public abstract class Handler<T> : IHandler<T> where T : class
{
    private IHandler<T> Next { get; set; }

    public virtual void Handle(T request)
    {
        Next?.Handle(request);
    }

    public IHandler<T> SetNext(IHandler<T> next)
    {
        Next = next;
        return Next;
    }
}

Important points:

  • Next is the next handler in the chain.
  • Handle is marked virtual to allow subclasses to override it.
  • SetNext returns Next to enable fluent chaining (fluent API).
  • The abstract implementation of Handle has no logic of its own — it just passes the request to the next handler.

3.3 Concrete validation handlers

Each validation condition becomes its own handler. Each handler has a single responsibility:

SocialSecurityNumberValidatorHandler:

public class SocialSecurityNumberValidatorHandler : Handler<User>
{
    private SocialSecurityNumberValidator socialSecurityNumberValidator
        = new SocialSecurityNumberValidator();

    public override void Handle(User request)
    {
        if (!socialSecurityNumberValidator.Validate(request.SocialSecurityNumber,
            request.CitizenshipRegion))
        {
            throw new UserValidationException("Social security number could not be validated");
        }
        base.Handle(request);  // Passe au handler suivant
    }
}

AgeValidationHandler:

public class AgeValidationHandler : Handler<User>
{
    public override void Handle(User user)
    {
        if (user.Age < 18)
        {
            throw new UserValidationException("You have to be 18 years or older");
        }

        base.Handle(user);
    }
}

NameValidationHandler:

public class NameValidationHandler : Handler<User>
{
    public override void Handle(User user)
    {
        if (user.Name.Length <= 1)
        {
            throw new UserValidationException("Your name is unlikely this short.");
        }

        base.Handle(user);
    }
}

CitizenshipRegionValidationHandler:

public class CitizenshipRegionValidationHandler : Handler<User>
{
    public override void Handle(User user)
    {
        if (user.CitizenshipRegion.TwoLetterISORegionName == "NO")
        {
            throw new UserValidationException("We currently do not support Norwegians");
        }

        base.Handle(user);
    }
}

Validation exception:

public class UserValidationException : Exception
{
    public UserValidationException(string message) : base(message)
    {
    }
}

Key mechanism: If validation succeeds, the handler calls base.Handle(request) to continue the chain. If it fails, it throws an exception and breaks the chain. It would also be possible to call base.Handle first in the method, depending on the desired order of execution.

3.4 Refactoring the UserProcessor

With the handlers in place, the UserProcessor is simplified:

public class UserProcessor
{
    public bool Register(User user)
    {
        try
        {
            var handler = new SocialSecurityNumberValidatorHandler();

            handler.SetNext(new AgeValidationHandler())
                .SetNext(new NameValidationHandler())
                .SetNext(new CitizenshipRegionValidationHandler());

            handler.Handle(user);
        }
        catch (UserValidationException ex)
        {
            return false;
        }

        return true;
    }
}

Handler chain constructed:

SocialSecurityNumberValidatorHandler
    → AgeValidationHandler
        → NameValidationHandler
            → CitizenshipRegionValidationHandler

Application entry point:

static void Main(string[] args)
{
    var user = new User("Filip Ekberg", 
        "870101XXXX", 
        new RegionInfo("SE"), 
        new DateTimeOffset(1987, 01, 29, 00, 00, 00, TimeSpan.FromHours(2)));

    var processor = new UserProcessor();

    var result = processor.Register(user);

    Console.WriteLine(result);
}

3.5 Benefits obtained

Moving on to the Chain of Responsibility pattern:

  1. Extensibility — The UserProcessor no longer knows the implementation details of each validation. To remove the NameValidationHandler, simply remove it from the chain.

  2. Testability — Each handler has a single responsibility. It is easier to write unit tests and ensure good code coverage.

  3. Simple reordering — If social security number validation is expensive (external API call paid, like in Sweden), we can reorder the handlers to validate age, name and citizenship first to reduce API calls. This reorganization is trivial with the boss.

  4. Object-oriented approach — Clean replacement of the if/else if/else block.


4. Demo 2 — Payment Processing

4.1 The data model

The payment system allows a user to pay for an order with multiple payment methods (PayPal, credit card, invoice) and even partial payments (eg: 50% PayPal + 50% invoice).

Order, Item, Payment model:

public class Order
{
    public Dictionary<Item, int> LineItems { get; } = new Dictionary<Item, int>();

    public IList<Payment> SelectedPayments { get; } = new List<Payment>();

    public IList<Payment> FinalizedPayments { get; } = new List<Payment>();

    // Montant dû = total des articles - total des paiements finalisés
    public decimal AmountDue => LineItems.Sum(item => item.Key.Price * item.Value) 
                              - FinalizedPayments.Sum(payment => payment.Amount);

    public ShippingStatus ShippingStatus { get; set; } = ShippingStatus.WaitingForPayment;
}

public enum ShippingStatus 
{ 
    WaitingForPayment,
    ReadyForShippment,
    Shipped
}

public enum PaymentProvider
{
    Paypal,
    CreditCard,
    Invoice
}

public class Payment
{
    public decimal Amount { get; set; }
    public PaymentProvider PaymentProvider { get; set; }
}

public class Item
{
    public string Id { get; }
    public string Name { get; }
    public decimal Price { get; }

    public Item(string id, string name, decimal price)
    {
        Id = id;
        Name = name;
        Price = price;
    }
}

4.2 Payment processors (IPaymentProcessor)

The interface common to all payment processors:

public interface IPaymentProcessor
{
    void Finalize(Order order);
}

PaypalPaymentProcessor:

public class PaypalPaymentProcessor : IPaymentProcessor
{
    public void Finalize(Order order)
    {
        // Invoque l'API PayPal pour finaliser le paiement

        var payment = order.SelectedPayments
            .FirstOrDefault(x => x.PaymentProvider == PaymentProvider.Paypal);

        if (payment == null) return;

        order.FinalizedPayments.Add(payment);
    }
}

InvoicePaymentProcessor:

public class InvoicePaymentProcessor : IPaymentProcessor
{
    public void Finalize(Order order)
    {
        // Crée une facture et l'envoie par email

        var payment = order.SelectedPayments
            .FirstOrDefault(x => x.PaymentProvider == PaymentProvider.Invoice);

        if (payment == null) return;

        order.FinalizedPayments.Add(payment);
    }
}

CreditCardPaymentProcessor:

public class CreditCardPaymentProcessor : IPaymentProcessor
{
    public void Finalize(Order order)
    {
        // Invoque l'API Stripe
        var payment = order.SelectedPayments
            .FirstOrDefault(x => x.PaymentProvider == PaymentProvider.CreditCard);

        if (payment == null) return;

        order.FinalizedPayments.Add(payment);
    }
}

Insufficient payment exception:

public class InsufficientPaymentException : Exception
{
}

4.3 The PaymentHandler abstract handler

The IHandler<T> interface for this demo:

public interface IHandler<T> where T : class
{
    IHandler<T> SetNext(IHandler<T> next);
    void Handle(T request);
}

The abstract PaymentHandler contains richer logic than the handler in demo 1. It checks if an amount is still due before invoking the next handler, and handles the case where there are no more handlers available but an amount remains to be paid:

public abstract class PaymentHandler : IHandler<Order>
{
    private IHandler<Order> Next { get; set; }

    public virtual void Handle(Order order)
    {
        Console.WriteLine($"Running: {GetType().Name}");

        if (Next == null && order.AmountDue > 0)
        {
            throw new InsufficientPaymentException();
        }

        if (order.AmountDue > 0)
        {
            Next.Handle(order);
        }
        else
        {
            order.ShippingStatus = ShippingStatus.ReadyForShippment;
        }
    }

    public IHandler<Order> SetNext(IHandler<Order> next)
    {
        Next = next;
        return Next;
    }
}

Handle method logic:

  1. If Next == null and AmountDue > 0 → there are no more handlers and the order is not paid → throw an InsufficientPaymentException.
  2. If AmountDue > 0 and there is a Next → call the next handler.
  3. If AmountDue == 0 → change order status to ReadyForShippment.

4.4 Concrete payment handlers

PaypalHandler:

public class PaypalHandler : PaymentHandler
{
    private PaypalPaymentProcessor PaypalPaymentProcessor { get; }
        = new PaypalPaymentProcessor();

    public override void Handle(Order order)
    {
        if (order.SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Paypal))
        {
            PaypalPaymentProcessor.Finalize(order);
        }

        base.Handle(order);  // Continue la chaîne
    }
}

InvoiceHandler:

public class InvoiceHandler : PaymentHandler
{
    public InvoicePaymentProcessor InvoicePaymentProcessor { get; }
        = new InvoicePaymentProcessor();

    public override void Handle(Order order)
    {
        if (order.SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Invoice))
        {
            InvoicePaymentProcessor.Finalize(order);
        }
        base.Handle(order);
    }
}

CreditCardHandler:

public class CreditCardHandler : PaymentHandler
{
    public CreditCardPaymentProcessor CreditCardPaymentProcessor { get; }
        = new CreditCardPaymentProcessor();

    public override void Handle(Order order)
    {
        if (order.SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.CreditCard))
        {
            CreditCardPaymentProcessor.Finalize(order);
        }

        base.Handle(order);
    }
}

4.5 The entry point: Program.cs

static void Main(string[] args)
{
    var order = new Order();
    order.LineItems.Add(new Item("ATOMOSV", "Atomos Ninja V", 499), 2);
    order.LineItems.Add(new Item("EOSR", "Canon EOS R", 1799), 1);

    order.SelectedPayments.Add(new Payment
    {
        PaymentProvider = PaymentProvider.Paypal,
        Amount = 1000
    });

    order.SelectedPayments.Add(new Payment
    {
        PaymentProvider = PaymentProvider.Invoice,
        Amount = 1797
    });

    Console.WriteLine(order.AmountDue);       // 2797
    Console.WriteLine(order.ShippingStatus);  // WaitingForPayment

    var handler = new PaypalHandler();
    handler.SetNext(new CreditCardHandler())
        .SetNext(new InvoiceHandler());

    handler.Handle(order);

    Console.WriteLine(order.AmountDue);       // 0
    Console.WriteLine(order.ShippingStatus);  // ReadyForShippment
}

Calculation of the total amount:

  • 2 × Atomos Ninja V at $499 = $998
  • 1 × Canon EOS R at $1799 = $1799
  • Total: $2797

Payment chain:

PaypalHandler (1000$)
    → CreditCardHandler (n/a, non sélectionné)
        → InvoiceHandler (1797$)

Execution result:

Running: PaypalHandler
Running: CreditCardHandler
Running: InvoiceHandler
0
ReadyForShippment

4.6 The intentional bug: breaking the chain

The trainer intentionally introduces a bug to illustrate a significant risk of the boss. The bug consisted of making a return in a handler when the payment method was not selected:

// VERSION BUGGÉE (ne pas faire)
public override void Handle(Order order)
{
    if (!order.SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Paypal))
    {
        return;  // PROBLÈME : brise la chaîne sans appeler base.Handle !
    }
    PaypalPaymentProcessor.Finalize(order);
    base.Handle(order);
}

With this bug, if PayPal is not selected, the chain is broken. The order does not receive an InsufficientPaymentException exception and remains unpaid. This is a silent problem and difficult to detect.

Key lesson: you should always make sure to call base.Handle (or the next handler) so as not to break the chain unintentionally.

Corrected version: We only call Finalize if the payment method is selected, but we always call base.Handle in all cases.


5. Demo 3 — Improvement of the Payment Processor (Handler/Receiver separation)

This demo improves the previous implementation by separating the handler from the receiver. Until now, the handler and the receiver were the same entity (linked list). In this version, the PaymentHandler has a list of receivers that process the request.

5.1 The new IReceiver interface

public interface IReceiver<T> where T : class
{
    void Handle(T request);
}

Difference with IHandler<T>: no SetNext method. The receiver is no longer responsible for passing the request to the next one — that is the role of the handler.

5.2 The refactored PaymentHandler

public class PaymentHandler
{
    private readonly IList<IReceiver<Order>> receivers;

    public PaymentHandler(params IReceiver<Order>[] receivers)
    {
        this.receivers = receivers;
    }

    public void Handle(Order order)
    {
        foreach (var receiver in receivers)
        {
            Console.WriteLine($"Running: {receiver.GetType().Name}");

            if (order.AmountDue > 0)
            {
                receiver.Handle(order);
            }
            else
            {
                break;  // Plus de montant dû, on sort de la boucle
            }
        }

        if (order.AmountDue > 0)
        {
            throw new InsufficientPaymentException();
        }
        else
        {
            order.ShippingStatus = ShippingStatus.ReadyForShippment;
        }
    }

    public void SetNext(IReceiver<Order> next)
    {
        receivers.Add(next);
    }
}

Advantages of this approach:

  • The PaymentHandler iterates over all receivers.
  • It controls the flow of the chain (it decides if and when to move to the next one).
  • Receivers no longer need to know the Chain of Responsibility pattern.
  • Receivers no longer have to call base.Handle to continue the chain.

5.3 Concrete receivers

Concrete handlers now inherit from IReceiver<Order> instead of PaymentHandler. They are simpler and uncorrelated from the chain mechanism:

PaypalHandler (receiver):

public class PaypalHandler : IReceiver<Order>
{
    private PaypalPaymentProcessor PaypalPaymentProcessor { get; }
        = new PaypalPaymentProcessor();

    public void Handle(Order order)
    {
        if (order.SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Paypal))
        {
            PaypalPaymentProcessor.Finalize(order);
        }
        // Pas besoin d'appeler base.Handle ou quoi que ce soit d'autre !
    }
}

InvoiceHandler (receiver):

public class InvoiceHandler : IReceiver<Order>
{
    public InvoicePaymentProcessor InvoicePaymentProcessor { get; }
        = new InvoicePaymentProcessor();

    public void Handle(Order order)
    {
        if (order.SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Invoice))
        {
            InvoicePaymentProcessor.Finalize(order);
        }
    }
}

CreditCardHandler (receiver):

public class CreditCardHandler : IReceiver<Order>
{
    public CreditCardPaymentProcessor CreditCardPaymentProcessor { get; }
        = new CreditCardPaymentProcessor();

    public void Handle(Order order)
    {
        if (order.SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.CreditCard))
        {
            CreditCardPaymentProcessor.Finalize(order);
        }
    }
}

5.4 The improved entry point

static void Main(string[] args)
{
    var order = new Order();
    order.LineItems.Add(new Item("ATOMOSV", "Atomos Ninja V", 499), 2);
    order.LineItems.Add(new Item("EOSR", "Canon EOS R", 1799), 1);

    order.SelectedPayments.Add(new Payment
    {
        PaymentProvider = PaymentProvider.Paypal,
        Amount = 1000
    });

    order.SelectedPayments.Add(new Payment
    {
        PaymentProvider = PaymentProvider.Invoice,
        Amount = 1797
    });

    Console.WriteLine(order.AmountDue);       // 2797
    Console.WriteLine(order.ShippingStatus);  // WaitingForPayment

    var handler = new PaymentHandler(
        new PaypalHandler(),
        new InvoiceHandler(), 
        new CreditCardHandler()
    );

    handler.Handle(order);

    Console.WriteLine(order.AmountDue);       // 0
    Console.WriteLine(order.ShippingStatus);  // ReadyForShippment
}

You can also add receivers after construction with SetNext:

// Alternative avec SetNext
var handler = new PaymentHandler(new PaypalHandler());
handler.SetNext(new InvoiceHandler());
handler.SetNext(new CreditCardHandler());

5.5 Advantages of Handler/Receiver separation

AppearanceDemo Approach 2 (linked list)Demo Approach 3 (list of receivers)
StructureEach handler knows the next oneThe handler manages all receivers in a list
Responsibility of the chainIn each handler (call base.Handle)In the central handler
Boss KnowledgeThe receiver must know the stringThe receiver doesn’t know it’s in a string
Risk of breaking the chainPresent (forgot base.Handle)Eliminated
FlexibilityOrder defined by stringOrder defined by list

The receiver no longer knows that it is executing through a Chain of Responsibility pattern. It is even more decoupled. To break the chain, simply throw an exception in a receiver — this will go back to the caller of the PaymentHandler.


6. Demo 4 — ILogger: existing implementation in .NET

The ASP.NET Core ILogger is a concrete example of implementing the Chain of Responsibility pattern in the .NET framework itself.

Configuration of logging providers in Program.cs:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            })
            .ConfigureLogging(logging =>
            {
                logging.ClearProviders();      // Efface tous les providers enregistrés
                logging.AddConsole();          // Console logger
                logging.AddDebug();            // Debug logger (fenêtre de débogage)
                logging.AddEventLog();         // Windows Event Log
            });
}

Use in a controller:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        // _logger.Log(...) enverra aux 3 providers en chaîne
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

How ​​it works:

  • When we call logger.Log(...), the framework goes through all registered providers and passes the message to them.
  • Each provider (console, debug, event log) decides whether to act based on severity and configuration.
  • Internally, .NET uses a logger list (similar to Demo 3) rather than a linked list.

You can also add external providers (logging to a database, Serilog, NLog, etc.) by simply adding an additional provider.

You may already be using the Chain of Responsibility pattern without knowing it, every time you configure logging in ASP.NET Core!


7. Demo 5 — ASP.NET Core processing pipeline (middlewares)

The ASP.NET Core middleware pipeline is another well-known implementation of the Chain of Responsibility pattern.

Concept illustration (Microsoft documentation):

Requête entrante
    → Middleware 1 (traitement) → next()
        → Middleware 2 (traitement) → next()
            → Middleware 3 (traitement) → next()
                ← Middleware 3 (finalisation)
            ← Middleware 2 (finalisation)
        ← Middleware 1 (finalisation)
Réponse sortante

Each middleware receives the request, performs its processing, calls next() to move on to the next one, then can perform processing on return.

Pipeline configuration in Startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.Use(async (context, next) => 
        {
            Console.WriteLine("Handling #1");
            await next.Invoke();            // Appelle le middleware suivant
            Console.WriteLine("Done #1");
        });

        app.Use(async (context, next) =>
        {
            Console.WriteLine("Handling #2");
            await next.Invoke();
            Console.WriteLine("Done #2");
        });

        app.Use(async (context, next) =>
        {
            Console.WriteLine("Handling #3");
            await next.Invoke();
            Console.WriteLine("Done #3");
        });
    }
}

Execution result during an HTTP call:

Handling #1
Handling #2
Handling #3
Done #3
Done #2
Done #1

Common middlewares in a standard ASP.NET Core application:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();  // Affiche les exceptions en dev
    }

    app.UseHttpsRedirection();   // Redirection HTTPS
    app.UseRouting();            // Résolution des routes
    app.UseAuthorization();      // Vérification des autorisations
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();  // Mappage vers les controllers
    });
}

Each of these Use... adds middleware to the processing chain. With each HTTP request, it goes through this entire chain.

Whether you’re building middleware or using everything ASP.NET Core offers, you’ll use the Chain of Responsibility pattern at one point or another.


8. Profits and compromises of the boss

Benefits

ProfitDescription
ExtensibilityEasy to add new handlers without modifying existing code
MaintainabilityEach handler has a single responsibility (single responsibility principle)
TestabilityIsolated handlers are easy to unit test with good code coverage
DecouplingThe sender does not know the implementation details of the receivers
DynamismThe order of handlers can be changed easily, unlike if/else
ClarityObject-oriented approach more readable than long strings of conditions
One or more active handlersOne or more handlers can process the same request

Compromise

CompromiseDescription
More codeIt is necessary to create interface classes, abstract and concrete
More complex debuggingExecution flow may be harder to follow
Risk of breaking the chainForgetting to call base.Handle can cause silent bugs
Unnecessary overheadFor simple if/else with few conditions, the pattern can be excessive

When to apply

Apply the Chain of Responsibility pattern when:

  • You have a sequence of validations or treatments to apply to an object.
  • You want each treatment to be easily replaceable, rearrangeable or testable.
  • Different handlers must potentially process the same request.
  • You want to decouple the code that triggers processing from the code that performs it.

Do not systematically replace all if/else blocks with this pattern. In some simple cases, an if/else is still entirely appropriate. It’s a question of judgment.


9. Summary and conclusion

Throughout this course, we discovered the Chain of Responsibility pattern through several angles:

Demo Summary

DemoContextIllustrated concept
Demo 1User ValidationBasic structure of the pattern (IHandler, abstract Handler, concrete handlers)
Demo 2Payment processingHandler with business logic, flow management, broken chain bug
Demo 3Improved payment processingHandler/Receiver separation, list of receivers, pattern-independent receivers
Demo 4ILogger in ASP.NET CoreExisting implementation in .NET: logger with list of providers
Demo 5ASP.NET Core PipelineExisting implementation in .NET: chained middleware

Fundamental concepts to remember

  1. The pattern replaces if/else if/else with an object-oriented approach where each condition becomes a handler.

  2. Two main variants:

  • Linked list (Demo 1 and 2): each handler knows the next one and calls it.
  • Handler with list of receivers (Demo 3): a central handler iterates over a list of receivers.
  1. Critical rule: never break the chain accidentally. If a handler needs to pass the request to the next one, it must always do so (via base.Handle or explicitly).

  2. Real applications: ILogger in .NET and ASP.NET Core middlewares are implementations of the pattern you probably already use.

Typical pattern architecture

[Sender / Client]
       |
       v
[Handler / IHandler<T>]
       |
  +---------+
  |         |
  v         v
[Receiver 1] [Receiver 2] ... [Receiver N]

Base code template

// 1. Interface
public interface IHandler<T> where T : class
{
    IHandler<T> SetNext(IHandler<T> next);
    void Handle(T request);
}

// 2. Classe abstraite
public abstract class Handler<T> : IHandler<T> where T : class
{
    private IHandler<T> Next { get; set; }

    public virtual void Handle(T request)
    {
        Next?.Handle(request);
    }

    public IHandler<T> SetNext(IHandler<T> next)
    {
        Next = next;
        return Next;
    }
}

// 3. Handlers concrets
public class ConcreteHandlerA : Handler<MyRequest>
{
    public override void Handle(MyRequest request)
    {
        if (/* condition */)
        {
            // Traitement spécifique
        }
        base.Handle(request);  // Toujours appeler le suivant !
    }
}

// 4. Construction de la chaîne
var handlerA = new ConcreteHandlerA();
handlerA.SetNext(new ConcreteHandlerB())
        .SetNext(new ConcreteHandlerC());

handlerA.Handle(myRequest);

This course allowed you to understand the Chain of Responsibility pattern, to implement it from scratch, to improve it, and to identify it in common frameworks. Play with the examples, extend the applications, and look for situations in your projects where this pattern can make your code more powerful, extensible, and testable.


Search Terms

c-sharp · design · patterns · chain · responsibility · testing · architecture · c# · .net · development · handler · payment · concrete · pattern · point · abstract · benefits · entry · handlers · interface · paymenthandler · processing · receiver · separation

Interested in this course?

Contact us to book it or get a custom training plan for your team.