Bộ lọc Filters trong ASP.NET Core Minimal API

Bộ lọc (Filters) trong ASP.NET Core Minimal API là các thành phần thực thi trước và/hoặc sau một route handler. Chúng cho phép bạn bổ sung các chức năng chung như kiểm tra hợp lệ (validation), ghi log, kiểm tra xác thực, hoặc sửa đổi request và response mà không phải lặp lại mã ở mọi endpoint.

Các công dụng của Filters:

  • Kiểm tra dữ liệu đầu vào
  • Ghi log request và response
  • Kiểm tra xác thực (authentication) hoặc phân quyền (authorization)
  • Sửa đổi dữ liệu request hoặc response
  • Xử lý ngoại lệ
  • Thực thi logic chung cho nhiều endpoint

Luồng thực thi:

Client Request<br>     │<br>     ▼<br>Endpoint Filter (Before)<br>     │<br>     ▼<br>Route Handler<br>     │<br>     ▼<br>Endpoint Filter (After)<br>     │<br>     ▼<br>Client Response

Nội dung trang:

Cách sử dụng filters trong Minimal API

Chúng ta sử dụng phương thức mở rộng AddEndpointFilter. Phương thức này nhận một Delegate đảm nhận hai vai trò cốt lõi:

  1. Nó nhận bối cảnh thực thi: EndpointFilterInvocationContext
  2. Nó trả về bước tiếp theo trong pipeline: EndpointFilterDelegate

EndpointFilterInvocationContext: cung cấp quyền truy cập trực tiếp vào HttpContext của request hiện tại và hé lộ một danh sách Arguments.

Arguments List: chứa các đối số được truyền vào route handler. Các đối số này được sắp xếp theo đúng thứ tự xuất hiện trong khai báo của handler. Một ví dụ điển hình được nêu trên tài liệu chính thức của Microsoft, xem bên dưới:

var builder = WebApplication.CreateBuilder(args);<br>var app = builder.Build();<br>string ColorName(string color) => $"Color specified: {color}!";<br>app.MapGet("/colorSelector/{color}", ColorName)<br>  .AddEndpointFilter(async (invocationContext, next) =><br>  {<br>    var color = invocationContext.GetArgument<string>(0);<br>    if(color == "Red")<br>    {<br>      return Results.Problem("Red not allowed!");<br>    }<br>    return await next(invocationContext);<br>  });<br>app.Run();

Giải thích:

Ở đây chúng ta đã định nghĩa endpoint handler:

string ColorName(string color) => $"Color specified: {color}!";

Đây là một phương thức đơn giản nhận một tham số string. Nếu endpoint thực thi thành công, nó trả về:

Color specified: Blue!<br><br>hoặc<br><br>Color specified: Green!

tùy thuộc vào URL.

Ánh xạ endpoint:

app.MapGet("/colorSelector/{color}", ColorName)

Đoạn này tạo ra một endpoint GET.

Các URL ví dụ:

GET /colorSelector/Blue<br>GET /colorSelector/Green<br>GET /colorSelector/Red

Phần {color} là một route parameter. Ví dụ:

/colorSelector/Blue

sẽ liên kết:

color = "Blue"

và truyền nó vào:

ColorName(color)

Thêm một Endpoint Filter:

.AddEndpointFilter(async (invocationContext, next) => {

Lệnh này gắn một filter chỉ cho endpoint này. Hãy hình dung thứ tự thực thi như sau:

Request<br>   ↓<br>Endpoint Filter<br>   ↓<br>Endpoint Handler (ColorName)<br>   ↓<br>Response

Bạn cũng có thể muốn tìm hiểu thêm – ASP.NET Core Minimal API Parameter Binding – hướng dẫn đầy đủ kèm mã.

Filter có thể:

  • kiểm tra các đối số
  • sửa đổi các đối số
  • dừng việc thực thi
  • sửa đổi response

Đọc đối số của endpoint:

var color = invocationContext.GetArgument<string>(0);

invocationContext chứa tất cả đối số được truyền vào endpoint. Endpoint là:

string ColorName(string color)

Các tham số của nó là:

Index 0 → color

Vì vậy:

GetArgument<string>(0)

sẽ trả về giá trị route. Nếu URL là:

/colorSelector/Blue

thì:

color == "Blue"

Kiểm tra giá trị:

if (color == "Red")<br>{<br>    return Results.Problem("Red not allowed!");<br>}

Nếu client gửi request:

GET /colorSelector/Red

filter sẽ ngay lập tức trả về:

Results.Problem(...)

mà không gọi endpoint. Endpoint handler không bao giờ được thực thi. Client nhận được một response tương tự:

{<br>    "title": "An error occurred.",<br>    "detail": "Red not allowed!"<br>}

Tiếp tục tới endpoint:

return await next(invocationContext);

next() gọi thành phần tiếp theo trong endpoint pipeline. Nếu không còn filter nào, nó sẽ gọi:

ColorName(color)

Vậy nên:

GET /colorSelector/Blue

sẽ thực thi:

ColorName("Blue")

và trả về:

Color specified: Blue!

Nhiều Endpoint Filters trong Minimal API

Chúng ta cũng có thể thêm nhiều Endpoint Filter cho Minimal API. Hãy hình dung chúng như những lớp bao quanh endpoint, giống như các hộp xếp chồng nhau. Xem đoạn mã bên dưới – bạn sẽ nhận thấy endpoint hầu như không làm gì cả. Phần thú vị chính là các filter được gắn vào nó.

var builder = WebApplication.CreateBuilder(args);<br>var app = builder.Build();<br>app.MapGet("/", () =><br>{<br>    app.Logger.LogInformation("           Endpoint");<br>    return "Test of multiple filters";<br>})<br>  .AddEndpointFilter(async (efiContext, next) =><br>  {<br>    app.Logger.LogInformation("Before 1st filter");<br>    var result = await next(efiContext);<br>    app.Logger.LogInformation("After 1st filter");<br>    return result;<br>  })<br>  .AddEndpointFilter(async (efiContext, next) =><br>  {<br>    app.Logger.LogInformation(" Before 2nd filter");<br>    var result = await next(efiContext);<br>    app.Logger.LogInformation(" After 2nd filter");<br>    return result;<br>  })<br>  .AddEndpointFilter(async (efiContext, next) =><br>  {<br>    app.Logger.LogInformation("     Before 3rd filter");<br>    var result = await next(efiContext);<br>    app.Logger.LogInformation("     After 3rd filter");<br>    return result;<br>  });<br>app.Run();

Giải thích:

Ánh xạ endpoint:

app.MapGet("/", () =><br>{<br>    app.Logger.LogInformation("           Endpoint");<br>    return "Test of multiple filters";<br>})

Đoạn này tạo một endpoint GET cho URL gốc (/).

Khi endpoint cuối cùng được thực thi, nó:

  • Ghi chữ “Endpoint” vào log.
  • Trả về chuỗi:

Test of multiple filters

Endpoint Filter thứ nhất:

.AddEndpointFilter(async (efiContext, next) =><br>{<br>    app.Logger.LogInformation("Before 1st filter");<br><br>    var result = await next(efiContext);<br><br>    app.Logger.LogInformation("After 1st filter");<br><br>    return result;<br>})

Filter này chạy trước endpoint. Trước khi gọi next():

app.Logger.LogInformation("Before 1st filter");

sẽ in ra:

Before first filter

Gọi next:

await next(efiContext);

Hướng dẫn đầy đủ về Minimal API Response trình bày các cách mà một endpoint Minimal API có thể trả dữ liệu cho client. Một handler có thể trả về một chuỗi đơn giản (được gửi dưới dạng plain text) hoặc một đối tượng (tự động được serialize sang JSON). Khi bạn cần kiểm soát mã trạng thái và thân response, bạn có thể trả về một IResult được dựng bằng các helper Results hoặc TypedResults, chẳng hạn như Ok, Created, NoContent, NotFound và BadRequest. Hướng dẫn này cũng đề cập tới file, stream và redirect, đồng thời giải thích cách khai báo các response có thể có của endpoint để các công cụ như Swagger có thể ghi chú chúng. Mỗi cách đều kèm ví dụ mã.

Lệnh này truyền việc thực thi cho filter tiếp theo. Sau khi filter đó (và cuối cùng là endpoint) hoàn tất, việc thực thi quay lại đây. Sau đó:

app.Logger.LogInformation("After 1st filter");

sẽ chạy.

Endpoint Filter thứ hai:

.AddEndpointFilter(async (efiContext, next) =><br>{<br>    app.Logger.LogInformation(" Before 2nd filter");<br><br>    var result = await next(efiContext);<br><br>    app.Logger.LogInformation(" After 2nd filter");<br><br>    return result;<br>})

Filter này hoạt động hoàn toàn giống filter thứ nhất. Nó bao quanh mọi thứ đứng sau nó.

Endpoint Filter thứ ba:

.AddEndpointFilter(async (efiContext, next) =><br>{<br>    app.Logger.LogInformation("     Before 3rd filter");<br>    var result = await next(efiContext);<br>    app.Logger.LogInformation("     After 3rd filter");<br>    return result;<br>});

Đây là filter cuối cùng.

Khi gọi:

await next(efiContext);

không gọi thêm filter nào nữa vì không còn filter nào. Thay vào đó, nó gọi endpoint.

Thứ tự thực thi:

Giả sử bạn request:

GET /

Bước 1: Filter đầu tiên bắt đầu.

Before first filter

Nó gọi:

await next()

Bước 2: Filter thứ hai bắt đầu.

Before 2nd filter

Nó gọi:

await next()

Bước 3: Filter thứ ba bắt đầu.

Before 3rd filter

Nó gọi:

await next()

Bước 4: Không còn filter nào, nên endpoint được thực thi.

Endpoint

Endpoint trả về:

Test of multiple filters

Bước 5: Việc thực thi quay lại filter thứ ba.

After 3rd filter

Bước 6: Việc thực thi quay lại filter thứ hai.

After 2nd filter

Bước 7: Việc thực thi quay lại filter đầu tiên.

After first filter

Log đầu ra cuối cùng: Các log xuất hiện theo thứ tự sau:

Before first filter<br> Before 2nd filter<br>     Before 3rd filter<br>           Endpoint<br>     After 3rd filter<br> After 2nd filter<br>After first filter

Kiểm tra dữ liệu trong Minimal API với Filters

Khi ứng dụng phát triển, việc kiểm tra các request đầu vào trở nên cần thiết để đảm bảo tính toàn vẹn của dữ liệu, độ tin cậy của ứng dụng và bảo mật. Một cách hiệu quả để triển khai validation trong Minimal API là sử dụng endpoint filter – cho phép nhà phát triển chặn các request trước khi chúng tới endpoint handler. Endpoint filter giúp logic kiểm tra dữ liệu được tập trung và tái sử dụng trên nhiều endpoint, giảm thiểu việc trùng lặp mã và cải thiện khả năng bảo trì.

Ví dụ dưới đây thực hiện kiểm tra dữ liệu với sự trợ giúp của filter:

app.MapPut("/works/{id}", async (int id, Work work, WorkDb db) =><br>{<br>    var todo = await db.Works.FindAsync(id);<br>    if (todo is null) return Results.NotFound();<br>    todo.Name = work.Name;<br>    todo.TimeStart = work.TimeStart;<br>    todo.TimeEnd = work.TimeEnd;<br>    todo.IsComplete = work.IsComplete;<br>    await db.SaveChangesAsync();<br>    return Results.NoContent();<br>}).AddEndpointFilter(async (efiContext, next) =><br>{<br>    var w = efiContext.GetArgument<Work>(1);<br>    var validationError = Utilities.IsValid(w);<br>    if (!string.IsNullOrEmpty(validationError))<br>    {<br>        return Results.Problem(validationError);<br>    }<br>    return await next(efiContext);<br>});

Mã của lớp Work:

public class Work<br>{<br>    public int Id { get; set; }<br><br>    public string Name { get; set; }<br><br>    public string TimeStart { get; set; }<br><br>    public string TimeEnd { get; set; }<br><br>    public bool IsComplete { get; set; }<br>}

Giải thích:

Sau khi định nghĩa endpoint, đoạn mã dưới đây gắn một endpoint filter:

.AddEndpointFilter(async (efiContext, next) =>

Endpoint filter thực thi trước và/hoặc sau endpoint handler. Nó có thể:

  • kiểm tra dữ liệu đầu vào
  • ghi log request
  • phân quyền người dùng
  • đo thời gian thực thi
  • sửa đổi response

Trong ví dụ này, nó thực hiện việc kiểm tra dữ liệu.

Truy cập đối tượng Request:

var w = efiContext.GetArgument<Work>(1);

GetArgument() lấy một trong các đối số của endpoint handler. Ở đây nó lấy đối tượng Work, vì Work là tham số thứ hai.

Kiểm tra đối tượng:

var validationError = Utilities.IsValid(w);

Phương thức Utilities.IsValid() thực hiện kiểm tra tùy chỉnh trên đối tượng Work.

Ví dụ, nó có thể kiểm tra rằng:

  • Name không được rỗng.
  • TimeStart phải sớm hơn TimeEnd.
  • Các trường bắt buộc phải có giá trị.

Nó trả về:

  • một thông báo lỗi nếu validation thất bại.
  • null hoặc chuỗi rỗng nếu validation thành công.

Trả về lỗi validation:

if (!string.IsNullOrEmpty(validationError))<br>{<br>    return Results.Problem(validationError);<br>}

Nếu validation thất bại, filter dừng request và trả về một HTTP error response chứa thông báo kiểm tra.

Ví dụ:

{<br>    "title": "An error occurred.",<br>    "detail": "TimeStart must be earlier than TimeEnd."<br>}

Endpoint handler không được thực thi khi validation thất bại.

Unit và Integration Testing trong Minimal APIs hướng dẫn cách kiểm thử các endpoint Minimal API ở hai cấp độ. Unit test kiểm tra logic của handler một cách độc lập, thường dùng framework như xUnit với các dependency được mock. Integration test chạy toàn bộ ứng dụng trong bộ nhớ bằng WebApplicationFactory và gửi các HTTP request thật để xác nhận routing, binding và response hoạt động cùng nhau. Các ví dụ mã cho thấy cách thiết lập và viết cả hai loại test.

Gọi endpoint:

return await next(efiContext);

Nếu validation thành công, filter gọi giai đoạn tiếp theo trong pipeline, vốn thực thi endpoint handler.

Luồng thực thi:

HTTP Request<br>     │<br>     ▼<br>Endpoint Filter<br>     │<br>     ├── Validation fails<br>     │       │<br>     │       ▼<br>     │   Return Problem()<br>     │<br>     └── Validation succeeds<br>               │<br>               ▼<br>     Endpoint Handler<br>               │<br>               ▼<br>     Update Database<br>               │<br>               ▼<br>     Return 204 No Content

Lợi ích của việc dùng Endpoint Filter để validation:

  • Tách biệt trách nhiệm: Logic validation được giữ tách biệt với logic nghiệp vụ của endpoint.
  • Tái sử dụng mã: Cùng một filter validation có thể áp dụng cho nhiều endpoint.
  • Handler gọn gàng hơn: Các phương thức endpoint tập trung vào việc xử lý request hợp lệ.
  • Xử lý lỗi nhất quán: Mọi lỗi validation đều có thể trả về định dạng response tiêu chuẩn hóa.
  • Cải thiện khả năng bảo trì: Quy tắc validation có thể cập nhật ở một nơi mà không phải sửa từng endpoint.

Triển khai giao diện IEndpointFilter

Ngoài việc được định nghĩa dưới dạng delegate, endpoint filter còn có thể được triển khai bằng cách tạo một lớp implements giao diện IEndpointFilter. Cách tiếp cận này gói logic filter vào một lớp có thể tái sử dụng, giúp dễ bảo trì và áp dụng trên nhiều endpoint hơn. Đoạn mã dưới đây minh họa filter validation ở trên được viết thành một lớp implements giao diện IEndpointFilter:

public class WorkIsValidFilter : IEndpointFilter<br>{<br>    private ILogger _logger;<br><br>    public WorkIsValidFilter(ILoggerFactory loggerFactory)<br>    {<br>        _logger = loggerFactory.CreateLogger<WorkIsValidFilter>();<br>    }<br><br>    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext,<br>        EndpointFilterDelegate next)<br>    {<br>        var work = efiContext.GetArgument<Work>(1);<br><br>        var validationError = Utilities.IsValid(work!);<br><br>        if (!string.IsNullOrEmpty(validationError))<br>        {<br>            _logger.LogWarning(validationError);<br>            return Results.Problem(validationError);<br>        }<br>        return await next(efiContext);<br>    }<br>}

Các filter implements giao diện IEndpointFilter có thể truy cập các service được đăng ký trong Dependency Injection (DI) container thông qua constructor injection hoặc service resolution, như đã minh họa ở ví dụ trên. Tuy nhiên, trong khi endpoint filter có thể dùng các dependency do DI cung cấp, chính các instance của filter lại không được resolve trực tiếp từ DI container.

Lớp “WorkIsValidFilter” được áp dụng cho các endpoint dưới đây:

app.MapPut("/works/{id}", async (int id, Work work, WorkDb db) =><br>{<br>    var todo = await db.Works.FindAsync(id);<br>    if (todo is null) return Results.NotFound();<br>    todo.Name = work.Name;<br>    todo.TimeStart = work.TimeStart;<br>    todo.TimeEnd = work.TimeEnd;<br>    todo.IsComplete = work.IsComplete;<br>    await db.SaveChangesAsync();<br>    return Results.NoContent();<br>}).AddEndpointFilter<WorkIsValidFilter>();

Authentication & Authorization trong Minimal API

Authentication (Xác thực) xác minh danh tính của người dùng trước khi cho phép truy cập API. Sau khi danh tính người dùng được thiết lập, Authorization (Phân quyền) xác định xem người dùng đã được xác thực có quyền truy cập các tài nguyên API cụ thể hay không.

Trong ASP.NET Core, việc phân quyền được xử lý bởi IAuthorizationService, được đăng ký khi bạn gọi phương thức mở rộng AddAuthorization.

Trong ví dụ sau, endpoint /hello được bảo vệ bởi một authorization policy. Để truy cập endpoint này, người dùng đã được xác thực phải đáp ứng hai yêu cầu:

  1. Thuộc vai trò “admin”.
  2. Có một claim scope với giá trị “head”.

Chỉ những người dùng đáp ứng cả hai điều kiện mới được phép truy cập tài nguyên /hello.

Đoạn mã dưới đây tạo một authorization policy mới tên LevelOne, bao hàm hai yêu cầu phân quyền:

  1. Một yêu cầu dựa trên vai trò thông qua RequireRole cho người dùng có vai trò admin.
  2. Một yêu cầu dựa trên claim thông qua RequireClaim, theo đó người dùng phải cung cấp một claim scope head.

Policy LevelOne được cung cấp như policy bắt buộc cho endpoint /hello:

using Microsoft.Identity.Web;<br><br>var builder = WebApplication.CreateBuilder(args);<br><br>builder.Services.AddAuthorizationBuilder()<br>  .AddPolicy("LevelOne", policy =><br>      policy<br>          .RequireRole("admin")<br>          .RequireClaim("scope", "head"));<br><br>var app = builder.Build();<br><br>app.MapGet("/hello", () => "Hello world!")<br>  .RequireAuthorization("LevelOne");<br><br>app.Run();

Dùng Endpoint Filter cho phân quyền tùy chỉnh

Filter rất hữu ích khi bạn cần các quy tắc phân quyền vượt ra ngoài hệ thống policy tích hợp sẵn. Ví dụ, giả sử chỉ chủ sở hữu của một work item mới được phép chỉnh sửa nó.

public class OwnerFilter : IEndpointFilter<br>{<br>    public async ValueTask<object?> InvokeAsync(<br>      EndpointFilterInvocationContext context,<br>      EndpointFilterDelegate next)<br>    {<br>        var httpContext = context.HttpContext;<br>        if (!httpContext.User.Identity!.IsAuthenticated)<br>        {<br>            return Results.Unauthorized();<br>        }<br>        var userId = httpContext.User.FindFirst("sub")?.Value;<br>        var work = context.GetArgument<Work>(1);<br>        if (work.OwnerId != userId)<br>        {<br>            return Results.Forbid();<br>        }<br>        return await next(context);<br>    }<br>}

Áp dụng filter:

app.MapPut("/works/{id}", UpdateWork)<br>    .AddEndpointFilter<OwnerFilter>()<br>    .RequireAuthorization();

Luồng thực thi:

Client Request<br>     │<br>     ▼<br>Authentication Middleware<br>     │<br>     ▼<br>Authorization Middleware<br>     │<br>     ▼<br>Endpoint Filter (Custom Rule)<br>     │<br>     ▼<br>Endpoint Handler<br>     │<br>     ▼<br>Database

Best Practice

Hãy sử dụng hệ thống authentication và authorization tích hợp sẵn để bảo vệ các Minimal API của bạn. Endpoint filter nên bổ sung cho hệ thống này bằng cách triển khai các quy tắc riêng của ứng dụng, chẳng hạn như xác minh quyền sở hữu tài nguyên, kiểm tra các ràng buộc nghiệp vụ, hoặc áp dụng các yêu cầu truy cập tùy chỉnh. Sự tách biệt này giúp ứng dụng của bạn an toàn, dễ bảo trì và phù hợp với các best practice của ASP.NET Core.

Kết luận

Endpoint filter là một tính năng mạnh mẽ của ASP.NET Core Minimal API, cung cấp một cách gọn gàng và có thể tái sử dụng để thực thi logic trước và sau endpoint handler. Chúng giúp tách các mối quan tâm cắt ngang – như validation, logging, authentication, authorization và xử lý ngoại lệ – khỏi logic nghiệp vụ cốt lõi, dẫn đến các triển khai endpoint sạch sẽ và dễ bảo trì hơn.

Bằng cách gói các chức năng chung vào filter, nhà phát triển có thể giảm thiểu trùng lặp mã, cải thiện tính nhất quán giữa các endpoint và đơn giản hóa việc bảo trì ứng dụng. Dù được triển khai dưới dạng delegate cho các tình huống đơn giản, hay dưới dạng lớp implements giao diện IEndpointFilter cho chức năng phức tạp và tái sử dụng hơn, endpoint filter nâng cao tính linh hoạt, khả năng đọc và khả năng mở rộng của các ứng dụng Minimal API – khiến chúng trở thành công cụ thiết yếu để xây dựng các web API vững chắc và dễ bảo trì.

Bài tutorial tiếp theo – Cách thực hiện Unit và Integration Testing trong Minimal APIs