Skip to content

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.

csharp
// 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();
ControllerMinimal API
Tổ chứcGom theo lớpGom theo file hoặc nhóm endpoint
Model bindingĐầy đủĐầy đủ
FilterAction filterEndpoint filter
OpenAPITự suy luận nhiều hơnKhai báo tường minh hơn
Hiệu năngRất tốtNhỉnh hơn một chút
Phù hợp vớiNhóm endpoint có logic chungVertical 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:

csharp
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.

csharp
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 requestMiddleware
Cần truy cập tham số đã bindEndpoint 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.

csharp
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ạnMặc địnhKhi nào chỉnh
MaxRequestBodySize~28,6 MBCho phép upload file lớn hơn
KeepAliveTimeout130 giâyHiếm khi cần đổi
MaxConcurrentConnectionsKhông giới hạnGầ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:

csharp
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.

csharp
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 pipeline

JSON 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 cachingCache phản hồi ở serverCao
Health checksLiveness và readinessCao
.NET AspireĐiều phối nhiều thành phần khi phát triểnTrung bình
HTTP loggingGhi log request và responseTrung bình
Request decompressionNhận body đã nén từ clientThấp
gRPCGiao tiếp nội bộ hiệu năng caoThấ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).

csharp
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ừ:

csharp
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ênChủ đềVì sao
CaoMinimal API với MapGroupCú pháp hiện đại, hợp vertical slice
CaoHealth checks đúng cáchCần cho mọi triển khai container
CaoOutput cachingGiảm tải rõ rệt với chi phí thấp
Trung bìnhEndpoint filterKhi dùng Minimal API
Trung bìnhNén phản hồiNếu không có proxy nén giúp
ThấpKestrel tuningMặ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:

csharp
[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:

csharp
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);
        });
    }
}
text
Trước: 32 dòng
Sau  : 20 dòng

Phần tiết kiệm được đến từ bốn chỗ:

text
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 OpenAPI

Và đây là lợi ích lớn hơn số dòng: phụ thuộc theo từng endpoint.

csharp
// 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) => ...);
text
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:

csharp
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:

csharp
[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");
    }
}
text
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 chung
text
Và 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:

csharp
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);
    }
}
csharp
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:

csharp
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());
    };
});
text
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 filterAction filter
Áp dụng choMột endpoint hoặc một nhómMột action, controller, hoặc toàn cục
Thấy tham số đã bindingCó — ctx.ArgumentsCó — ActionArguments
Chi phíThấp — không qua bộ máy MVCCao hơn
Đổi kết quảTrả về một object khácGán context.Result
Thứ tựTheo thứ tự đăng kýTheo Order và phạm vi
text
Đ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:

csharp
// 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.

text
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, idempotency
text
Chọ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 tra

Bà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:

csharp
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();
bash
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/leads

Kết quả đo trên ASP.NET Core 9 (.NET 9.0.203), 500 bản ghi lead dạng JSON:

text
  none  :   117.927 byte
  gzip  :     5.854 byte  Content-Encoding: gzip
  br    :     2.603 byte  Content-Encoding: br
Kích thướcGiảmSo với không nén
Không nén117.927 B—1×
gzip5.854 B95,0%20,1×
brotli2.603 B97,8%45,3×

Tỉ lệ nén cao bất thường — và lý do đáng hiểu:

text
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 45

Nên đừng chờ đợi 45 lần trên hệ thống thật. Nhưng ngay cả 5 lần cũng đáng:

text
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út

Brotli tốt hơn gzip khoảng 2,2 lần ở bài đo này — nhưng có cái giá:

text
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
csharp
// 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.

text
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
csharp
// 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ỏ.

text
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 byte

3. Có reverse proxy hoặc CDN đứng trước — và nó đã nén.

text
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ụng

Một cạm bẫy bảo mật đáng biết: EnableForHttps mặc định TẮT.

csharp
o.EnableForHttps = true;      // phải bật tường minh
text
Lý 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.

text
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:

  1. Minimal API đầy đủ tính năng — chọn nó hay controller là vấn đề tổ chức code.
  2. 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.
  3. Chống quá tải bằng rate limiting, không bằng giới hạn kết nối.

Tham khảo ​

Điều hướng ​

Phát hành theo giấy phép MIT.