Chain of Responsibility in C# Request Pipelines
This article explains how to implement the Chain of Responsibility design pattern to build flexible, decoupled request-handling pipelines in C#. You will learn the core architectural components of the pattern, see a step-by-step code implementation using an abstract base handler, and explore how to wire handlers together dynamically to process, validate, or terminate incoming requests.
Core Concept
The Chain of Responsibility is a behavioral design pattern that passes a request along a sequence of handlers. Upon receiving a request, each handler decides either to process it and pass it to the next handler in the chain, or to halt execution (short-circuit) if validation or conditions fail.
A standard implementation consists of three primary elements:
- Request Object: The data or context passed through the chain.
- Handler Interface/Base Class: The contract defining how handlers link to one another and process requests.
- Concrete Handlers: Individual classes containing specific processing logic (e.g., authentication, logging, validation).
Step 1: Define the Request Context
Create a request class that encapsulates the data passing through the pipeline.
public class OrderRequest
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public string UserRole { get; set; } = string.Empty;
public bool IsProcessed { get; set; }
}Step 2: Create the Handler Abstraction
Define an interface or an abstract base class. An abstract base class is often preferred because it can manage the reference to the next handler and enforce default propagation behavior.
public interface IOrderHandler
{
IOrderHandler SetNext(IOrderHandler nextHandler);
void Handle(OrderRequest request);
}
public abstract class BaseOrderHandler : IOrderHandler
{
private IOrderHandler? _nextHandler;
public IOrderHandler SetNext(IOrderHandler nextHandler)
{
_nextHandler = nextHandler;
return nextHandler; // Allows fluent chaining: h1.SetNext(h2).SetNext(h3)
}
public virtual void Handle(OrderRequest request)
{
// Default behavior: pass to the next handler if it exists
_nextHandler?.Handle(request);
}
}Step 3: Implement Concrete Handlers
Each concrete handler encapsulates a single responsibility and decides whether to continue execution.
Authentication/Authorization Handler
public class AuthorizationHandler : BaseOrderHandler
{
public override void Handle(OrderRequest request)
{
Console.WriteLine("Executing AuthorizationHandler...");
if (request.UserRole != "Admin" && request.UserRole != "Customer")
{
Console.WriteLine("Pipeline Terminated: Unauthorized user role.");
return; // Short-circuit: Do not call base.Handle(request)
}
base.Handle(request);
}
}Validation Handler
public class ValidationHandler : BaseOrderHandler
{
public override void Handle(OrderRequest request)
{
Console.WriteLine("Executing ValidationHandler...");
if (request.Amount <= 0)
{
Console.WriteLine("Pipeline Terminated: Order amount must be greater than zero.");
return; // Short-circuit
}
base.Handle(request);
}
}Business Logic Handler
public class PaymentProcessingHandler : BaseOrderHandler
{
public override void Handle(OrderRequest request)
{
Console.WriteLine("Executing PaymentProcessingHandler...");
request.IsProcessed = true;
Console.WriteLine($"Payment of {request.Amount:C} processed successfully for Order #{request.OrderId}.");
base.Handle(request);
}
}Step 4: Assemble and Execute the Pipeline
Link the handlers in the required execution sequence and pass the request into the head of the chain.
public class Program
{
public static void Main()
{
// 1. Instantiate handlers
var authHandler = new AuthorizationHandler();
var validationHandler = new ValidationHandler();
var paymentHandler = new PaymentProcessingHandler();
// 2. Build the pipeline: Auth -> Validation -> Payment
authHandler
.SetNext(validationHandler)
.SetNext(paymentHandler);
// 3. Test a valid request
Console.WriteLine("--- Processing Valid Request ---");
var validRequest = new OrderRequest
{
OrderId = 101,
Amount = 150.00m,
UserRole = "Customer"
};
authHandler.Handle(validRequest);
// 4. Test an invalid request (short-circuit demonstration)
Console.WriteLine("\n--- Processing Invalid Request ---");
var invalidRequest = new OrderRequest
{
OrderId = 102,
Amount = -25.00m,
UserRole = "Customer"
};
authHandler.Handle(invalidRequest);
}
}Key Benefits
- Single Responsibility Principle: Each handler manages only one specific operation (e.g., authentication, logging, validation).
- Open/Closed Principle: New handlers can be introduced into the pipeline without modifying existing handler logic or the client calling the pipeline.
- Dynamic Reconfiguration: Handlers can be arranged, added, or removed at runtime depending on configuration or environment settings.