Chain of Responsibility trong C#: Pipeline và Middleware
Chain of Responsibility trong C# / .NET 10: dựng pipeline validation kiểu ASP.NET Core middleware, với handler có thể forward hoặc dừng chuỗi.
Mục lục
- Chain of Responsibility giải quyết bài toán gì?
- Cấu trúc giáo khoa trong .NET 10 trông thế nào?
- ASP.NET Core middleware dùng pattern này thế nào?
- Khi nào Chain of Responsibility bắn trật?
- So sánh Chain of Responsibility với Decorator và Strategy thế nào?
- Một ví dụ thật trong .NET 10 trông thế nào?
- Đọc tiếp gì trong series?
Endpoint checkout phải chạy sáu validation trước khi charge thẻ:
cart không rỗng, customer đã verify, địa chỉ shipping hợp lệ, hàng
còn stock, mã giảm giá áp được, total trong giới hạn fraud. Phiên
bản đầu là một method to với câu if (!check) return BadRequest(...)
xếp chồng nhau. Lần thứ năm ai đó cần thêm rule "giữa discount và
fraud", method không đọc nổi.
Chain of Responsibility pattern là câu trả lời. Mỗi rule thành handler một việc: xét request, quyết định pass tiếp hay dừng chuỗi với lỗi. Bản thân chuỗi chỉ là thứ tự. Thêm rule mới là một handler mới cộng một dòng đăng ký. Middleware ASP.NET Core là ví dụ nổi tiếng nhất pattern này trong .NET; bài học ở đây là bạn áp cùng hình dạng cho mọi pipeline.
Tiếp tục với checkout e-commerce: bài này chỉ pattern dạng chuỗi validate request.
Chain of Responsibility giải quyết bài toán gì?
Pattern xứng chỗ khi một request phải chảy qua nhiều handler theo thứ tự, mỗi handler có thể xử, sửa, hoặc dừng chuỗi. Ba hình dạng cụ thể:
- Pipeline validate. Mỗi rule short-circuit khi fail; request không bao giờ tới action handler nếu rule fail.
- Pipeline tiền xử lý. Decompress, decrypt, deserialize, authenticate. Mỗi bước làm việc và forward.
- Routing fallback. Thử handler chính; nếu
NotApplicable, thử thứ cấp; lạiNotApplicable, dùng default.
Cái không phải bài toán Chain: "mọi wrapper luôn forward" — đó là Decorator. "Tôi muốn fan out nhiều handler và gom mọi response" — gần với Observer hoặc list phẳng. Chain đặc biệt nói về thứ tự tuyến tính với short-circuit optional.
Cấu trúc giáo khoa trong .NET 10 trông thế nào?
Một interface handler cộng next tường minh:
public sealed record CheckoutRequest(string CartId, string CustomerId, decimal Total);
public sealed record CheckoutValidation(bool Ok, string? Error);
public delegate Task<CheckoutValidation> CheckoutHandler(
CheckoutRequest req, CancellationToken ct);
public interface ICheckoutRule
{
Task<CheckoutValidation> HandleAsync(
CheckoutRequest req, CheckoutHandler next, CancellationToken ct);
}
Mỗi rule quyết định gọi next hay trả về sớm:
public sealed class CartNotEmptyRule : ICheckoutRule
{
private readonly ICartRepository _carts;
public CartNotEmptyRule(ICartRepository carts) => _carts = carts;
public async Task<CheckoutValidation> HandleAsync(
CheckoutRequest req, CheckoutHandler next, CancellationToken ct)
{
var cart = await _carts.GetAsync(req.CartId, ct);
if (cart is null || cart.Items.Count == 0)
return new(false, "Cart is empty");
return await next(req, ct);
}
}
public sealed class FraudLimitRule : ICheckoutRule
{
public Task<CheckoutValidation> HandleAsync(
CheckoutRequest req, CheckoutHandler next, CancellationToken ct)
=> req.Total > 100_000m
? Task.FromResult(new CheckoutValidation(false, "Above fraud limit"))
: next(req, ct);
}
Chuỗi build bằng fold rule từ phải sang trái, với handler cuối trả success:
public sealed class CheckoutValidator
{
private readonly CheckoutHandler _chain;
public CheckoutValidator(IEnumerable<ICheckoutRule> rules)
{
CheckoutHandler terminal = (_, __) => Task.FromResult(new CheckoutValidation(true, null));
_chain = rules.Reverse().Aggregate(terminal,
(next, rule) => (req, ct) => rule.HandleAsync(req, next, ct));
}
public Task<CheckoutValidation> ValidateAsync(CheckoutRequest req, CancellationToken ct)
=> _chain(req, ct);
}
Bức tranh cấu trúc:
flowchart LR
Req[CheckoutRequest] --> R1[CartNotEmptyRule]
R1 -->|next| R2[CustomerVerifiedRule]
R2 -->|next| R3[InStockRule]
R3 -->|next| R4[FraudLimitRule]
R4 -->|next| Done[Success]
R1 -. short-circuit .-> Fail[Lỗi: cart rỗng]
R3 -. short-circuit .-> Fail
R4 -. short-circuit .-> Fail
Mỗi rule là node. Đa số chảy qua; bất kỳ cái nào dừng được chuỗi.
ASP.NET Core middleware dùng pattern này thế nào?
Pipeline ASP.NET Core là cùng hình dạng với type framework cụ thể:
app.Use(async (ctx, next) =>
{
if (string.IsNullOrEmpty(ctx.Request.Headers["X-Tenant"]))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsync("Missing tenant header");
return; // short-circuit
}
await next(); // forward
});
app.Use(async (ctx, next) =>
{
var sw = System.Diagnostics.Stopwatch.StartNew();
await next();
Console.WriteLine($"{ctx.Request.Path}: {sw.ElapsedMilliseconds}ms");
});
app.MapControllers();
Mỗi app.Use thêm handler. next là phần còn lại pipeline.
Short-circuit bằng cách không gọi next. Nhận diện middleware là
Chain of Responsibility cho biết mọi điều bạn biết về middleware
đều áp cho pipeline custom — và ngược lại.
Khi nào Chain of Responsibility bắn trật?
Ba bẫy:
- Thứ tự ngầm và mong manh. "FraudLimit phải chạy sau TotalCalculation" là rule chỉ sống ở thứ tự đăng ký. Dev mới đặt rule sai chỗ thì fraud check không thấy total đã chiết khấu. Tài liệu hoá thứ tự; cân nhắc khai báo dependency tường minh nếu chuỗi to.
- Handler với chéo chuỗi. Một rule mutate context share để nói với rule sau. Chuỗi thành chuỗi coupling. Truyền record immutable forward; nếu rule cần thêm thông tin, trả record mới với phần thêm.
- Lỗi nuốt lặng lẽ. Handler trả
falsetrống không thông báo; caller thấy "validation failed" không chi tiết. Luôn kèm thông tin lỗi hành động được; log là không đủ.
So sánh Chain of Responsibility với Decorator và Strategy thế nào?
| Pattern | Luôn forward? | Short-circuit? | Số lớp |
|---|---|---|---|
| Chain of Responsibility | Không | Có | Nhiều, có thứ tự |
| Decorator | Có | Không (hoặc hiếm) | Nhiều, có thứ tự |
| Strategy | n/a | n/a | Một (thay) |
Quy tắc rõ nhất: nếu wrapper có thể không gọi next, bạn có
Chain of Responsibility; nếu mọi wrapper luôn forward, bạn có
Decorator. Strategy ở category khác — nó chọn một thuật toán
chứ không nối nhiều.
Một ví dụ thật trong .NET 10 trông thế nào?
Wire mọi rule với DI và dùng validator trước khi charge:
public sealed record CheckoutRequest(string CartId, string CustomerId, decimal Total);
public sealed record CheckoutValidation(bool Ok, string? Error);
public delegate Task<CheckoutValidation> CheckoutHandler(CheckoutRequest req, CancellationToken ct);
public interface ICheckoutRule
{
Task<CheckoutValidation> HandleAsync(CheckoutRequest req, CheckoutHandler next, CancellationToken ct);
}
public sealed class CartNotEmptyRule : ICheckoutRule { /* xem trên */ }
public sealed class CustomerVerifiedRule : ICheckoutRule { /* ... */ }
public sealed class InStockRule : ICheckoutRule { /* ... */ }
public sealed class FraudLimitRule : ICheckoutRule { /* xem trên */ }
public sealed class CheckoutValidator
{
private readonly CheckoutHandler _chain;
public CheckoutValidator(IEnumerable<ICheckoutRule> rules)
{
CheckoutHandler terminal = (_, __) => Task.FromResult(new CheckoutValidation(true, null));
_chain = rules.Reverse().Aggregate(terminal, (next, rule) =>
(req, ct) => rule.HandleAsync(req, next, ct));
}
public Task<CheckoutValidation> ValidateAsync(CheckoutRequest req, CancellationToken ct)
=> _chain(req, ct);
}
// Composition root - thứ tự quan trọng!
builder.Services.AddScoped<ICheckoutRule, CartNotEmptyRule>();
builder.Services.AddScoped<ICheckoutRule, CustomerVerifiedRule>();
builder.Services.AddScoped<ICheckoutRule, InStockRule>();
builder.Services.AddScoped<ICheckoutRule, FraudLimitRule>();
builder.Services.AddScoped<CheckoutValidator>();
// Caller
[ApiController, Route("checkout")]
public sealed class CheckoutController : ControllerBase
{
private readonly CheckoutValidator _validator;
private readonly ICheckoutService _checkout;
public CheckoutController(CheckoutValidator v, ICheckoutService c) => (_validator, _checkout) = (v, c);
[HttpPost]
public async Task<IActionResult> Post(CheckoutRequest req, CancellationToken ct)
{
var v = await _validator.ValidateAsync(req, ct);
if (!v.Ok) return BadRequest(v.Error);
var result = await _checkout.CheckoutAsync(new(req.CartId, "stripe"), ct);
return Ok(result);
}
}
// Test một rule riêng lẻ
[Fact]
public async Task FraudLimit_blocks_above_threshold()
{
var sut = new FraudLimitRule();
var nextCalled = false;
CheckoutHandler next = (_, __) => { nextCalled = true; return Task.FromResult(new CheckoutValidation(true, null)); };
var result = await sut.HandleAsync(new("c1", "u1", 200_000m), next, default);
Assert.False(result.Ok);
Assert.False(nextCalled);
}
Mỗi rule test riêng được. Thêm rule mới là một class cộng một dòng DI. Thứ tự chuỗi là thứ tự đăng ký — làm thứ tự đó tường minh trong code review.
Đọc tiếp gì trong series?
- Bài trước: Proxy — pattern structural cuối.
- Bài kế: Command — đóng gói request thành object để queue, undo, hay replay.
- Tham chiếu chéo: Decorator — cùng hình, nhưng mọi wrapper forward.
- Tham chiếu chéo: Mediator — khi pattern fan out sang nhiều handler chọn theo type message thay vì chạy qua chúng theo thứ tự.
- Cây quyết định: Cách chọn design pattern phù hợp.
Một takeaway thực dụng: một khi bạn dựng được pipeline middleware ASP.NET, bạn dựng được mọi Chain of Responsibility. Framework cho bạn pattern; design pattern để bạn tái dùng cùng mô hình tâm trí ở mọi nơi pipeline là hình dạng đúng.
Câu hỏi thường gặp
Chain of Responsibility khác Decorator thế nào?
ASP.NET Core middleware có thật là Chain of Responsibility không?
app.Use((ctx, next) => ...) thêm handler có thể làm việc trước call, quyết định gọi next hay không, và làm việc sau. Đó là pattern giáo khoa với hình dạng RequestDelegate riêng của ASP.NET. Biết pattern chuyển ngay: mọi pipeline bạn dựng cho validation, audit, transformation đều theo cùng cấu trúc.Khi nào dùng Chain of Responsibility thay vì list validator?
IEnumerable<IValidator> phẳng cộng Where(v => !v.IsValid) ổn khi validator độc lập và bạn muốn gom mọi lỗi thay vì dừng ở lỗi đầu.Test handler riêng lẻ trong Chain of Responsibility thế nào?
next có chạy. Bản thân chuỗi nối ở composition; integration test một lần với input thực.