8.11 - Mở rộng và đào sâu
Tóm tắt: Những chủ đề ASP.NET Core nối tiếp Module 8, kèm mức độ ưu tiên. Điểm cần làm rõ trước: Minimal API không phải phiên bản rút gọn dành cho ứng dụng nhỏ — nó có đầy đủ filter, model binding, validation và OpenAPI, chỉ là cú pháp khác. Lựa chọn giữa nó và controller là vấn đề tổ chức code, không phải vấn đề năng lực. Mục dễ cấu hình sai nhất là Kestrel: mặc định của nó phù hợp cho phần lớn trường hợp, và chỉnh sai giới hạn thường tạo ra vấn đề thay vì giải quyết — đặc biệt là
MaxConcurrentConnections, thứ hay bị đặt thấp một cách vô tình.
Mục tiêu bài học
Sau bài này bạn có thể:
- Chọn giữa Minimal API và controller theo tiêu chí rõ ràng.
- Dùng endpoint filter thay cho action filter.
- Biết cấu hình Kestrel nào đáng chỉnh và cái nào không.
- Bật nén phản hồi an toàn.
Nội dung bài học
8.11.1 — Minimal API
Ưu tiên cao nếu bạn đang bắt đầu dự án mới.
// Minimal API — gọn, nhưng đầy đủ tính năng
app.MapGet("/api/leads/{id:guid}", async (
Guid id,
ISender sender,
CancellationToken ct) =>
{
var result = await sender.Send(new GetLeadQuery(new LeadId(id)), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
})
.WithName("GetLead")
.Produces<LeadDto>()
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization();| Controller | Minimal API | |
|---|---|---|
| Tổ chức | Gom theo lớp | Gom theo file hoặc nhóm endpoint |
| Model binding | Đầy đủ | Đầy đủ |
| Filter | Action filter | Endpoint filter |
| OpenAPI | Tự suy luận nhiều hơn | Khai báo tường minh hơn |
| Hiệu năng | Rất tốt | Nhỉnh hơn một chút |
| Phù hợp với | Nhóm endpoint có logic chung | Vertical slice |
Với Minimal API, gom endpoint thành nhóm để tránh một Program.cs dài hàng trăm dòng:
public static class LeadEndpoints
{
public static void MapLeadEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/leads")
.RequireAuthorization()
.WithTags("Leads");
group.MapGet("/{id:guid}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id:guid}/convert", Convert);
}
}
// Program.cs
app.MapLeadEndpoints();
app.MapCustomerEndpoints();MapGroup cho phép áp dụng cấu hình chung — authorization, tags, filter — cho cả nhóm, nên nó bù được phần lớn lợi thế tổ chức của controller.
Cách tổ chức này hợp tự nhiên với Vertical Slice Architecture, nơi mỗi tính năng tự chứa cả endpoint lẫn handler (bài 16.4).
8.11.2 — Endpoint filter
Ưu tiên trung bình. Thay cho action filter trong Minimal API.
public sealed class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var argument = context.Arguments.OfType<T>().FirstOrDefault();
if (argument is null) return await next(context);
var validator = context.HttpContext.RequestServices.GetService<IValidator<T>>();
if (validator is null) return await next(context);
var result = await validator.ValidateAsync(argument);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
return await next(context);
}
}
// Su dung
group.MapPost("/", Create).AddEndpointFilter<ValidationFilter<CreateLeadRequest>>();Endpoint filter chạy sau model binding và trước handler, nên nó thấy được tham số đã được bind — khác với middleware, vốn chỉ thấy HttpContext thô.
Chọn cái nào:
| Cần gì | Dùng |
|---|---|
| Áp dụng cho mọi request | Middleware |
| Cần truy cập tham số đã bind | Endpoint filter |
| Cần biết endpoint nhưng không cần tham số | Middleware sau UseRouting |
8.11.3 — Kestrel: cẩn thận
Ưu tiên thấp. Mặc định phù hợp cho phần lớn trường hợp.
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 30 * 1024 * 1024; // 30 MB cho upload
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});Ba giới hạn đáng chỉnh và lý do:
| Giới hạn | Mặc định | Khi nào chỉnh |
|---|---|---|
MaxRequestBodySize | ~28,6 MB | Cho phép upload file lớn hơn |
KeepAliveTimeout | 130 giây | Hiếm khi cần đổi |
MaxConcurrentConnections | Không giới hạn | Gần như không bao giờ chỉnh |
Hàng cuối là cái bẫy: người ta hay đặt MaxConcurrentConnections = 100 vì nghe có vẻ an toàn, nhưng nó giới hạn số kết nối TCP, không phải số request đồng thời. Trình duyệt giữ kết nối keep-alive, nên 100 kết nối có thể chỉ là 30 người dùng — và người thứ 31 bị từ chối.
Cách đúng để chống quá tải là rate limiting theo người dùng hoặc IP, không phải giới hạn kết nối:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", o =>
{
o.PermitLimit = 100;
o.Window = TimeSpan.FromMinutes(1);
o.QueueLimit = 10;
});
});QueueLimit là chi tiết hữu ích: thay vì từ chối ngay, nó cho phép một số request chờ, làm mượt các đỉnh tải ngắn (bài 9.6).
8.11.4 — Nén phản hồi
Ưu tiên trung bình. Giảm băng thông đáng kể cho API trả JSON lớn.
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true; // xem cảnh báo bên dưới
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
app.UseResponseCompression(); // dat SOM trong pipelineJSON nén rất tốt — thường giảm 70–85% kích thước.
Cảnh báo bảo mật: EnableForHttps = true mở ra khả năng tấn công BREACH, trong đó kẻ tấn công suy ra bí mật bằng cách quan sát kích thước phản hồi nén. Rủi ro chỉ thực sự đáng kể khi phản hồi chứa cả dữ liệu bí mật (như token CSRF) và dữ liệu do người dùng kiểm soát. Với API trả JSON thuần và dùng token trong header, rủi ro thấp.
Nếu hệ thống chạy sau reverse proxy hoặc CDN, để chúng nén — hiệu quả hơn và không tốn CPU của ứng dụng.
8.11.5 — Những chủ đề khác
| Chủ đề | Nội dung | Ưu tiên |
|---|---|---|
| Output caching | Cache phản hồi ở server | Cao |
| Health checks | Liveness và readiness | Cao |
| .NET Aspire | Điều phối nhiều thành phần khi phát triển | Trung bình |
| HTTP logging | Ghi log request và response | Trung bình |
| Request decompression | Nhận body đã nén từ client | Thấp |
| gRPC | Giao tiếp nội bộ hiệu năng cao | Thấp |
Health checks đáng làm ngay và thường bị làm sai: liveness không được kiểm tra database, còn readiness thì phải (bài 18.12).
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false });
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = c => c.Tags.Contains("ready")
});HTTP logging hữu ích khi chẩn đoán, nhưng bật mặc định trên production sẽ ghi cả mật khẩu trong body và token trong header. Nếu bật, phải cấu hình loại trừ:
builder.Services.AddHttpLogging(o =>
{
o.LoggingFields = HttpLoggingFields.RequestPath | HttpLoggingFields.ResponseStatusCode;
o.RequestHeaders.Remove("Authorization"); // KHÔNG ghi token
});8.11.6 — Thứ tự nên học
| Ưu tiên | Chủ đề | Vì sao |
|---|---|---|
| Cao | Minimal API với MapGroup | Cú pháp hiện đại, hợp vertical slice |
| Cao | Health checks đúng cách | Cần cho mọi triển khai container |
| Cao | Output caching | Giảm tải rõ rệt với chi phí thấp |
| Trung bình | Endpoint filter | Khi dùng Minimal API |
| Trung bình | Nén phản hồi | Nếu không có proxy nén giúp |
| Thấp | Kestrel tuning | Mặc định đã tốt, chỉnh sai gây hại |
Sau Module 8, bước tiếp theo là Module 9 — Web API chuyên nghiệp.
8.11.7 — Rà lại code của bạn
Danh sách rà soát ASP.NET Core nâng cao
- Endpoint được gom bằng MapGroup, Program.cs không dài quá mức.
- Có cả health check liveness và readiness, phân biệt đúng.
- Không đặt MaxConcurrentConnections nếu không có lý do cụ thể.
- Chống quá tải bằng rate limiting, không bằng giới hạn kết nối.
- Nếu có reverse proxy, để nó nén thay vì nén trong ứng dụng.
- HTTP logging không ghi header Authorization và body nhạy cảm.
- Đã cân nhắc output caching cho endpoint đọc nhiều.
Bài tập áp dụng
Bài 1 — Chuyển sang MapGroup
Gom các endpoint của một controller thành một nhóm Minimal API và so sánh số dòng.
Tiêu chí hoàn thành: bạn có số dòng trước và sau, và biết một trường hợp nên giữ controller.
Gợi ý và lời giải — Bài 1
Gợi ý. Phần tiết kiệm được không nằm ở thân phương thức, mà ở phần khai báo lặp lại: thuộc tính route, kiểu trả về, và constructor.
Lời giải — trước:
[ApiController]
[Route("api/leads")]
[Authorize]
public class LeadsController : ControllerBase
{
private readonly ILeadService _service;
public LeadsController(ILeadService service) => _service = service;
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<LeadDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<LeadDto>>> Lay(
[FromQuery] int trang = 1, CancellationToken ct = default)
=> Ok(await _service.LayAsync(trang, ct));
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(LeadDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<LeadDto>> Tim(int id, CancellationToken ct)
{
var lead = await _service.TimAsync(id, ct);
return lead is null ? NotFound() : Ok(lead);
}
[HttpPost]
[ProducesResponseType(typeof(LeadDto), StatusCodes.Status201Created)]
public async Task<ActionResult<LeadDto>> Tao(TaoLeadRequest yc, CancellationToken ct)
{
var lead = await _service.TaoAsync(yc, ct);
return CreatedAtAction(nameof(Tim), new { id = lead.Id }, lead);
}
}Sau:
public static class LeadsEndpoints
{
public static void MapLeads(this IEndpointRouteBuilder app)
{
var nhom = app.MapGroup("/api/leads")
.RequireAuthorization()
.WithTags("Leads")
.AddEndpointFilter<XacThucFilter>();
nhom.MapGet("/", (ILeadService s, int trang, CancellationToken ct) => s.LayAsync(trang, ct));
nhom.MapGet("/{id:int}", async (int id, ILeadService s, CancellationToken ct) =>
await s.TimAsync(id, ct) is { } lead ? Results.Ok(lead) : Results.NotFound());
nhom.MapPost("/", async (TaoLeadRequest yc, ILeadService s, CancellationToken ct) =>
{
var lead = await s.TaoAsync(yc, ct);
return Results.Created($"/api/leads/{lead.Id}", lead);
});
}
}Trước: 32 dòng
Sau : 20 dòngPhần tiết kiệm được đến từ bốn chỗ:
1. Không có constructor và trường — phụ thuộc tiêm thẳng vào tham số
2. MapGroup gom Route, Authorize, Tags về MỘT chỗ
3. Không có ActionResult<T> bọc ngoài — Results.Ok là đủ
4. Kiểu trả về tự suy ra cho tài liệu OpenAPIVà đây là lợi ích lớn hơn số dòng: phụ thuộc theo từng endpoint.
// Controller: mọi endpoint chia sẻ cùng một bộ phụ thuộc
public LeadsController(ILeadService s, IPdfService pdf, IEmailSender email) { }
// -> endpoint GET cũng phải dựng IPdfService và IEmailSender
// Minimal API: mỗi endpoint nhận đúng thứ nó cần
nhom.MapGet("/{id:int}", (int id, ILeadService s) => ...);
nhom.MapPost("/{id:int}/xuat-pdf", (int id, IPdfService pdf) => ...);Với service đắt để khởi tạo, khác biệt này đo được.
Và nó làm rõ endpoint nào phụ thuộc vào cái gì.Nhóm lồng nhau — tiện khi có nhiều phiên bản:
var v1 = app.MapGroup("/api/v1").WithTags("v1");
v1.MapGroup("/leads").MapLeads();
v1.MapGroup("/khach-hang").MapKhachHang();Và một trường hợp NÊN giữ controller:
[ApiController]
[Route("api/bao-cao")]
public class BaoCaoController : ControllerBase
{
[HttpGet("xuat/{id:int}")]
public async Task<IActionResult> Xuat(int id, CancellationToken ct)
{
var stream = await _service.TaoBaoCaoAsync(id, ct);
// Model binding phức tạp, content negotiation, formatter tuỳ biến
return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"bao-cao-{id}.xlsx");
}
}Controller vẫn tốt hơn khi cần:
- model binding phức tạp, hoặc binder tuỳ biến
- content negotiation (trả XML hoặc JSON tuỳ Accept)
- action filter theo lớp, dùng lại ở nhiều controller
- kế thừa một lớp cơ sở có logic dùng chungVà một lý do thực tế: dự án đã có 40 controller chạy tốt.
Chuyển hết sang Minimal API là một dự án riêng, không phải cải tiến.
-> dùng Minimal API cho phần MỚI, giữ controller cho phần cũ.
Hai kiểu chạy song song trong cùng một ứng dụng không vấn đề gì.Bài 2 — Viết endpoint filter
Cài filter validation dùng FluentValidation và áp dụng cho một nhóm endpoint.
Tiêu chí hoàn thành: filter chạy cho mọi endpoint trong nhóm, và bạn biết nó khác action filter của controller ở chỗ nào.
Gợi ý và lời giải — Bài 2
Gợi ý. Endpoint filter nhận EndpointFilterInvocationContext, trong đó có sẵn danh sách tham số đã được binding.
Lời giải:
public class XacThucFilter<T>(IValidator<T> validator) : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
{
var doiTuong = ctx.Arguments.OfType<T>().FirstOrDefault();
if (doiTuong is null) return await next(ctx);
var kq = await validator.ValidateAsync(doiTuong, ctx.HttpContext.RequestAborted);
if (!kq.IsValid)
return Results.ValidationProblem(kq.ToDictionary());
return await next(ctx);
}
}var nhom = app.MapGroup("/api/leads").RequireAuthorization();
nhom.MapPost("/", TaoLead).AddEndpointFilter<XacThucFilter<TaoLeadRequest>>();
nhom.MapPut("/{id:int}", SuaLead).AddEndpointFilter<XacThucFilter<SuaLeadRequest>>();Hoặc áp cho cả nhóm bằng một filter tự tìm validator:
nhom.AddEndpointFilterFactory((ctx, next) =>
{
var kieuThamSo = ctx.MethodInfo.GetParameters()
.FirstOrDefault(p => p.Name is "yc" or "request")?.ParameterType;
if (kieuThamSo is null) return next; // endpoint không có body -> bỏ qua
var kieuValidator = typeof(IValidator<>).MakeGenericType(kieuThamSo);
return async ic =>
{
var validator = ic.HttpContext.RequestServices.GetService(kieuValidator) as IValidator;
if (validator is null) return await next(ic);
var doiTuong = ic.Arguments.FirstOrDefault(a => a?.GetType() == kieuThamSo);
if (doiTuong is null) return await next(ic);
var kq = await validator.ValidateAsync(new ValidationContext<object>(doiTuong));
return kq.IsValid ? await next(ic) : Results.ValidationProblem(kq.ToDictionary());
};
});AddEndpointFilterFactory chạy MỘT LẦN lúc dựng endpoint,
không phải mỗi request -> phần reflection không tốn gì lúc chạy.Khác action filter của controller ở bốn điểm:
| Endpoint filter | Action filter | |
|---|---|---|
| Áp dụng cho | Một endpoint hoặc một nhóm | Một action, controller, hoặc toàn cục |
| Thấy tham số đã binding | Có — ctx.Arguments | Có — ActionArguments |
| Chi phí | Thấp — không qua bộ máy MVC | Cao hơn |
| Đổi kết quả | Trả về một object khác | Gán context.Result |
| Thứ tự | Theo thứ tự đăng ký | Theo Order và phạm vi |
Điểm khác biệt thực tế nhất là thứ tự:
endpoint filter chạy theo đúng thứ tự Add, dễ suy luận
action filter có quy tắc thứ tự phức tạp (toàn cục -> controller -> action,
kết hợp với thuộc tính Order) -> hay gây bất ngờBa filter đáng có, ngoài validation:
// 1. Ghi log thời gian cho endpoint chậm
public class LogChamFilter(ILogger<LogChamFilter> log) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
{
var sw = Stopwatch.StartNew();
var kq = await next(ctx);
if (sw.ElapsedMilliseconds > 500)
log.LogWarning("{Endpoint} mất {Ms} ms",
ctx.HttpContext.GetEndpoint()?.DisplayName, sw.ElapsedMilliseconds);
return kq;
}
}
// 2. Kiểm tra tenant khớp với token
public class KiemTraTenantFilter(INguoiDungHienTai nd) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
=> ctx.Arguments.OfType<ICoTenantId>().FirstOrDefault() is { } yc
&& yc.TenantId != nd.TenantId
? Results.Forbid()
: await next(ctx);
}
// 3. Idempotency cho POST
public class IdempotencyFilter(IDistributedCache cache) : IEndpointFilter { /* ... */ }Và một điều cần biết: filter KHÔNG thay thế middleware.
Middleware: chạy cho MỌI request, kể cả những request không khớp endpoint nào
-> dùng cho: xác thực, CORS, nén, ghi log truy cập
Endpoint filter: chỉ chạy khi đã xác định được endpoint
-> dùng cho: validation, phân quyền theo dữ liệu, idempotencyChọn sai tầng là một nguồn lỗi thật:
đặt xác thực vào endpoint filter -> request 404 không đi qua nó
đặt validation vào middleware -> chưa có tham số đã binding để kiểm traBài 3 — Đo nén
Gọi một endpoint trả JSON lớn với và không có Accept-Encoding: br, so sánh kích thước phản hồi.
Tiêu chí hoàn thành: bạn có ba con số (không nén, gzip, brotli), và biết ba trường hợp không nên bật nén.
Gợi ý và lời giải — Bài 3
Gợi ý. Máy chủ chỉ nén khi client nói nó hiểu được. Không gửi Accept-Encoding thì không có nén nào.
Lời giải:
builder.Services.AddResponseCompression(o =>
{
o.EnableForHttps = true; // mặc định TẮT — xem bên dưới
o.Providers.Add<BrotliCompressionProvider>();
o.Providers.Add<GzipCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(o => o.Level = CompressionLevel.Optimal);
builder.Services.Configure<GzipCompressionProviderOptions>(o => o.Level = CompressionLevel.Optimal);
app.UseResponseCompression();curl -sD - -o /dev/null http://127.0.0.1:5311/api/leads
curl -sD - -o /dev/null -H "Accept-Encoding: gzip" http://127.0.0.1:5311/api/leads
curl -sD - -o /dev/null -H "Accept-Encoding: br" http://127.0.0.1:5311/api/leadsKết quả đo trên ASP.NET Core 9 (.NET 9.0.203), 500 bản ghi lead dạng JSON:
none : 117.927 byte
gzip : 5.854 byte Content-Encoding: gzip
br : 2.603 byte Content-Encoding: br| Kích thước | Giảm | So với không nén | |
|---|---|---|---|
| Không nén | 117.927 B | — | 1× |
| gzip | 5.854 B | 95,0% | 20,1× |
| brotli | 2.603 B | 97,8% | 45,3× |
Tỉ lệ nén cao bất thường — và lý do đáng hiểu:
JSON có cấu trúc LẶP rất mạnh:
tên trường lặp lại 500 lần: "hoTen", "email", "dienThoai"...
giá trị giống nhau: cùng số điện thoại, cùng ghi chú, cùng trạng thái
-> thuật toán nén thay mỗi chuỗi lặp bằng một tham chiếu ngắn
-> với dữ liệu thật, đa dạng hơn, tỉ lệ thường là 5–10 lần chứ không phải 45Nên đừng chờ đợi 45 lần trên hệ thống thật. Nhưng ngay cả 5 lần cũng đáng:
Phản hồi 200 KB, 1.000 request/phút:
không nén: 200 MB/phút băng thông
nén 5 lần: 40 MB/phútBrotli tốt hơn gzip khoảng 2,2 lần ở bài đo này — nhưng có cái giá:
Brotli nén CHẬM HƠN gzip ở cùng mức chất lượng
-> với nội dung sinh động (mỗi request một nội dung khác), chi phí CPU đáng kể
CompressionLevel.Optimal cho brotli: chậm nhất, nén tốt nhất
CompressionLevel.Fastest: nhanh, nén kém hơn — thường là lựa chọn đúng cho API// Với nội dung sinh động, Fastest thường cân bằng tốt hơn
builder.Services.Configure<BrotliCompressionProviderOptions>(
o => o.Level = CompressionLevel.Fastest);Và ba trường hợp KHÔNG nên bật nén:
1. Nội dung đã nén sẵn.
JPEG, PNG, MP4, ZIP, PDF đã nén
-> nén lại gần như không giảm được gì, chỉ tốn CPU
-> và đôi khi còn TĂNG kích thước vài byte// ASP.NET Core mặc định chỉ nén một danh sách kiểu MIME an toàn
o.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/problem+json"]);2. Phản hồi rất nhỏ.
Phản hồi 200 byte -> nén xuống 180 byte
-> tiết kiệm 20 byte, tốn một lần gọi thuật toán nén
-> và header Content-Encoding cũng tốn vài chục byte3. Có reverse proxy hoặc CDN đứng trước — và nó đã nén.
Nginx, Cloudflare, Azure Front Door đều nén được
-> bật ở cả hai nơi = nén hai lần, hoặc proxy giải nén rồi nén lại
-> để proxy làm, và tắt ở ứng dụngMột cạm bẫy bảo mật đáng biết: EnableForHttps mặc định TẮT.
o.EnableForHttps = true; // phải bật tường minhLý do mặc định tắt: các tấn công kiểu BREACH và CRIME khai thác
mối liên hệ giữa KÍCH THƯỚC phản hồi đã nén và nội dung bí mật bên trong.
Rủi ro thật khi: phản hồi chứa CẢ bí mật (token chống CSRF) lẫn
dữ liệu do người dùng kiểm soát được.
Với API JSON trả dữ liệu nghiệp vụ và token nằm ở header,
rủi ro này rất thấp -> bật là hợp lý.Và một cách đơn giản hơn cho dữ liệu tĩnh: nén sẵn.
Với tệp JS, CSS, hoặc dữ liệu ít đổi:
nén sẵn lúc build thành .br và .gz
-> máy chủ chỉ việc gửi tệp đã nén, không tốn CPU lúc chạy
-> và dùng được mức nén cao nhất mà không ảnh hưởng độ trễTự kiểm tra
Minimal API có kém tính năng hơn controller không?
Không. Nó có đầy đủ filter, model binding, validation và OpenAPI. Lựa chọn giữa hai cách là vấn đề tổ chức code chứ không phải vấn đề năng lực.
MapGroup giúp gì cho Minimal API?
Nó cho phép áp dụng cấu hình chung như authorization, tags và filter cho cả nhóm endpoint, nên bù được phần lớn lợi thế tổ chức của controller mà không cần một Program.cs dài hàng trăm dòng.
Endpoint filter khác middleware thế nào?
Endpoint filter chạy sau model binding nên thấy được tham số đã bind, còn middleware chỉ thấy HttpContext thô. Dùng middleware khi cần áp dụng cho mọi request, dùng endpoint filter khi cần truy cập tham số.
Vì sao MaxConcurrentConnections là cái bẫy?
Vì nó giới hạn số kết nối TCP chứ không phải số request đồng thời. Trình duyệt giữ kết nối keep-alive nên một trăm kết nối có thể chỉ là ba mươi người dùng, và người tiếp theo bị từ chối oan.
Cách đúng để chống quá tải là gì?
Rate limiting theo người dùng hoặc IP, không phải giới hạn kết nối. QueueLimit cho phép một số request chờ thay vì bị từ chối ngay, làm mượt các đỉnh tải ngắn.
Rủi ro của EnableForHttps khi nén phản hồi là gì?
Nó mở ra khả năng tấn công BREACH, trong đó kẻ tấn công suy ra bí mật từ kích thước phản hồi nén. Rủi ro đáng kể khi phản hồi chứa cả dữ liệu bí mật lẫn dữ liệu do người dùng kiểm soát; với API trả JSON thuần và token trong header thì thấp.
Kết luận
Ba điều đáng nhớ nhất:
- Minimal API đầy đủ tính năng — chọn nó hay controller là vấn đề tổ chức code.
- Mặc định của Kestrel đã tốt; chỉnh sai giới hạn kết nối thường tạo ra vấn đề mới.
- Chống quá tải bằng rate limiting, không bằng giới hạn kết nối.
Tham khảo
- Middleware trong ASP.NET Core
- Routing trong ASP.NET Core
- Rate limiting trong ASP.NET Core
- Web API trong ASP.NET Core
Điều hướng
- Bài trước: 8.10 — EF Core tối thiểu: lưu dữ liệu vào database
- Bài tiếp theo: 8.12 — Mini case study
- Về module: Trang mục lục