Learnixo

.NET & C# Development · Lesson 43 of 229

Chain of Responsibility — Pass Requests Along a Chain

Chain of Responsibility — Pass Requests Along a Chain

Chain of Responsibility passes a request along a chain of handlers. Each handler decides whether to handle the request or pass it to the next handler. The sender doesn't know which handler will ultimately process it.


Classic Implementation

C#
// Handler base
public abstract class ApprovalHandler
{
    private ApprovalHandler? _next;

    public ApprovalHandler SetNext(ApprovalHandler next)
    {
        _next = next;
        return next;   // enables fluent chaining
    }

    public abstract Task HandleAsync(ApprovalRequest request);

    protected Task PassToNextAsync(ApprovalRequest request)
        => _next?.HandleAsync(request) ?? Task.CompletedTask;
}

public record ApprovalRequest(int Amount, string Requester);

// Concrete handlers — each handles requests within its authority
public class TeamLeadApproval : ApprovalHandler
{
    public override Task HandleAsync(ApprovalRequest request)
    {
        if (request.Amount <= 1_000)
        {
            Console.WriteLine($"TeamLead approved £{request.Amount} for {request.Requester}");
            return Task.CompletedTask;
        }
        Console.WriteLine($"TeamLead cannot approve £{request.Amount} — escalating");
        return PassToNextAsync(request);
    }
}

public class ManagerApproval : ApprovalHandler
{
    public override Task HandleAsync(ApprovalRequest request)
    {
        if (request.Amount <= 10_000)
        {
            Console.WriteLine($"Manager approved £{request.Amount} for {request.Requester}");
            return Task.CompletedTask;
        }
        return PassToNextAsync(request);
    }
}

public class DirectorApproval : ApprovalHandler
{
    public override Task HandleAsync(ApprovalRequest request)
    {
        Console.WriteLine($"Director approved £{request.Amount} for {request.Requester}");
        return Task.CompletedTask;
    }
}

// Build the chain
var teamLead = new TeamLeadApproval();
var manager  = new ManagerApproval();
var director = new DirectorApproval();

teamLead.SetNext(manager).SetNext(director);

// Send requests
await teamLead.HandleAsync(new ApprovalRequest(500,   "Alice"));   // TeamLead approves
await teamLead.HandleAsync(new ApprovalRequest(5_000, "Bob"));     // Manager approves
await teamLead.HandleAsync(new ApprovalRequest(50_000,"Carol"));   // Director approves

Functional Pipeline (Modern C# Style)

C#
// Chain as a list of middleware functions
public class RequestPipeline<T>
{
    private readonly List<Func<T, Func<Task>, Task>> _handlers = new();

    public RequestPipeline<T> Use(Func<T, Func<Task>, Task> handler)
    {
        _handlers.Add(handler);
        return this;
    }

    public async Task RunAsync(T context)
    {
        int index = 0;

        async Task Next()
        {
            if (index < _handlers.Count)
            {
                var handler = _handlers[index++];
                await handler(context, Next);
            }
        }

        await Next();
    }
}

// Usage — identical to ASP.NET Core middleware
var pipeline = new RequestPipeline<HttpContext>()
    .Use(async (ctx, next) =>
    {
        Console.WriteLine("Auth check");
        if (!ctx.User.IsAuthenticated) { ctx.Response.StatusCode = 401; return; }
        await next();
    })
    .Use(async (ctx, next) =>
    {
        Console.WriteLine("Rate limit check");
        await next();
    })
    .Use(async (ctx, next) =>
    {
        Console.WriteLine("Handler — process request");
        await next();
    });

ASP.NET Core Middleware = Chain of Responsibility

C#
// ASP.NET Core's middleware pipeline IS the Chain of Responsibility pattern
app.Use(async (context, next) =>
{
    Console.WriteLine("Before request");
    await next();             // pass to next handler
    Console.WriteLine("After response");
});

app.Use(async (context, next) =>
{
    if (context.Request.Headers["X-Api-Key"].ToString() != "secret")
    {
        context.Response.StatusCode = 401;
        return;   // short-circuit: don't call next()
    }
    await next();
});

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello!");
});

Validation Chain

C#
public abstract class Validator<T>
{
    private Validator<T>? _next;

    public Validator<T> Then(Validator<T> next) { _next = next; return next; }

    public ValidationResult Validate(T input)
    {
        var result = Check(input);
        if (!result.IsValid) return result;
        return _next?.Validate(input) ?? ValidationResult.Success();
    }

    protected abstract ValidationResult Check(T input);
}

public class NotEmptyValidator : Validator<string>
{
    protected override ValidationResult Check(string s)
        => string.IsNullOrWhiteSpace(s)
            ? ValidationResult.Failure("Value cannot be empty")
            : ValidationResult.Success();
}

public class MaxLengthValidator(int max) : Validator<string>
{
    protected override ValidationResult Check(string s)
        => s.Length > max
            ? ValidationResult.Failure($"Value exceeds maximum length of {max}")
            : ValidationResult.Success();
}

// Chain validators
var validator = new NotEmptyValidator();
validator.Then(new MaxLengthValidator(100));

var result = validator.Validate("Hello");

Interview Answer

"Chain of Responsibility passes a request along a chain of handlers — each handler either processes it or forwards it. The ASP.NET Core middleware pipeline is the most widely known implementation: Use adds a middleware that calls next() to continue, or short-circuits by returning. In application code, the pattern applies to approval workflows (escalate by authority level), validation pipelines (each validator checks one rule and passes to the next), and permission checks (stop early on deny). The key advantage over a single large if/else: each handler is independent and testable in isolation, and adding a new handler doesn't require modifying existing ones. The key trade-off: no guarantee that a request is handled — if no handler matches and the chain ends without a handler, the request is silently dropped."