Table of Contents
- Starting point: the problem to be solved
- The IHandler interface and the abstract Handler class
- Concrete validation handlers
- UserProcessor refactoring
- Benefits Obtained
- The data model
- Payment Processors (IPaymentProcessor)
- The PaymentHandler abstract handler
- Concrete payment handlers
- The entry point: Program.cs
- The intentional bug: breaking the chain
- The new IReceiver interface
- The PaymentHandler refactored
- Concrete receivers
- The improved entry point
- Advantages of Handler/Receiver separation
- Demo 4 — ILogger: existing implementation in .NET
- Demo 5 — ASP.NET Core Processing Pipeline (middlewares)
- Boss’s benefits and compromises
- 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:
| Element | Role |
|---|---|
| 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, aFileLogger, aDatabaseLogger.
Basic rules:
- The receiver contains the logic to decide whether to act on the request or not.
- The handler ensures that each receiver receives the request.
- Even if the request is not processed by a particular receiver, it is still forwarded to the next one in the chain.
- 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:
- Social security number must be valid (depends on region/country).
- User must be 18 years or older.
- The name must be more than one character.
- 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:
Nextis the next handler in the chain.Handleis markedvirtualto allow subclasses to override it.SetNextreturnsNextto enable fluent chaining (fluent API).- The abstract implementation of
Handlehas 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:
-
Extensibility — The
UserProcessorno longer knows the implementation details of each validation. To remove theNameValidationHandler, simply remove it from the chain. -
Testability — Each handler has a single responsibility. It is easier to write unit tests and ensure good code coverage.
-
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.
-
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:
- If
Next == nullandAmountDue > 0→ there are no more handlers and the order is not paid → throw anInsufficientPaymentException. - If
AmountDue > 0and there is aNext→ call the next handler. - If
AmountDue == 0→ change order status toReadyForShippment.
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
PaymentHandleriterates 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.Handleto 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
| Appearance | Demo Approach 2 (linked list) | Demo Approach 3 (list of receivers) |
|---|---|---|
| Structure | Each handler knows the next one | The handler manages all receivers in a list |
| Responsibility of the chain | In each handler (call base.Handle) | In the central handler |
| Boss Knowledge | The receiver must know the string | The receiver doesn’t know it’s in a string |
| Risk of breaking the chain | Present (forgot base.Handle) | Eliminated |
| Flexibility | Order defined by string | Order 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
| Profit | Description |
|---|---|
| Extensibility | Easy to add new handlers without modifying existing code |
| Maintainability | Each handler has a single responsibility (single responsibility principle) |
| Testability | Isolated handlers are easy to unit test with good code coverage |
| Decoupling | The sender does not know the implementation details of the receivers |
| Dynamism | The order of handlers can be changed easily, unlike if/else |
| Clarity | Object-oriented approach more readable than long strings of conditions |
| One or more active handlers | One or more handlers can process the same request |
Compromise
| Compromise | Description |
|---|---|
| More code | It is necessary to create interface classes, abstract and concrete |
| More complex debugging | Execution flow may be harder to follow |
| Risk of breaking the chain | Forgetting to call base.Handle can cause silent bugs |
| Unnecessary overhead | For 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
| Demo | Context | Illustrated concept |
|---|---|---|
| Demo 1 | User Validation | Basic structure of the pattern (IHandler, abstract Handler, concrete handlers) |
| Demo 2 | Payment processing | Handler with business logic, flow management, broken chain bug |
| Demo 3 | Improved payment processing | Handler/Receiver separation, list of receivers, pattern-independent receivers |
| Demo 4 | ILogger in ASP.NET Core | Existing implementation in .NET: logger with list of providers |
| Demo 5 | ASP.NET Core Pipeline | Existing implementation in .NET: chained middleware |
Fundamental concepts to remember
-
The pattern replaces if/else if/else with an object-oriented approach where each condition becomes a handler.
-
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.
-
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.Handleor explicitly). -
Real applications:
ILoggerin .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