Filter (bộ lọc) trong ứng dụng Minimal API
Nguồn: Filters in Minimal API apps
Filter (bộ lọc) trong Minimal API cho phép các nhà phát triển triển khai business logic (logic nghiệp vụ) hỗ trợ các tác vụ sau:
- Chạy code trước và sau khi endpoint handler thực thi
- Kiểm tra và sửa đổi các tham số được cung cấp trong quá trình gọi endpoint handler
- Chặn hành vi response của endpoint handler
Filter hữu ích trong nhiều tình huống:
- Kiểm tra tham số request và body được gửi đến endpoint
- Ghi log thông tin về request và response
- Kiểm tra request có nhắm đến phiên bản API được hỗ trợ không
Bài viết này mô tả cách dùng filter trong ứng dụng Minimal API, ví dụ như kiểm tra dữ liệu request và ghi log response.
Làm việc với Filter
Filter được đăng ký bằng cách cung cấp Delegate (ủy thác) nhận vào EndpointFilterInvocationContext và trả về EndpointFilterDelegate. EndpointFilterInvocationContext cung cấp truy cập vào HttpContext của request và danh sách Arguments. Danh sách này xác định các đối số được truyền vào handler theo thứ tự xuất hiện trong khai báo handler.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string ColorName(string color) => $"Color specified: {color}!";
app.MapGet("/colorSelector/{color}", ColorName)
.AddEndpointFilter(async (invocationContext, next) =>
{
var color = invocationContext.GetArgument<string>(0);
if (color == "Red")
{
return Results.Problem("Red not allowed!");
}
return await next(invocationContext);
});
app.Run();Code trên:
- Gọi extension method
AddEndpointFilterđể thêm filter vào endpoint/colorSelector/{color}. - Trả về màu được chỉ định, ngoại trừ giá trị
"Red". - Trả về
Results.Problemkhi/colorSelector/Redđược request. - Dùng
nextlàmEndpointFilterDelegatevàinvocationContextlàmEndpointFilterInvocationContextđể gọi filter tiếp theo trong pipeline, hoặc request delegate nếu filter cuối cùng đã được gọi.
Filter chạy trước endpoint handler. Khi nhiều lần gọi AddEndpointFilter trên một handler:
- Thứ tự thực thi của code filter được gọi trước lời gọi
EndpointFilterDelegate(next) là First In, First Out (FIFO - vào trước ra trước). - Thứ tự thực thi của code filter được gọi sau lời gọi
EndpointFilterDelegate(next) là First In, Last Out (FILO - vào trước ra sau).
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
app.Logger.LogInformation(" Endpoint");
return "Test of multiple filters";
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation("Before first filter");
var result = await next(efiContext);
app.Logger.LogInformation("After first filter");
return result;
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation(" Before 2nd filter");
var result = await next(efiContext);
app.Logger.LogInformation(" After 2nd filter");
return result;
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation(" Before 3rd filter");
var result = await next(efiContext);
app.Logger.LogInformation(" After 3rd filter");
return result;
});
app.Run();Trong code trên, các filter và endpoint ghi log output sau:
Before first filter
Before 2nd filter
Before 3rd filter
Endpoint
After 3rd filter
After 2nd filter
After first filterCode sau dùng filter triển khai interface IEndpointFilter:
using Filters.EndpointFilters;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
app.Logger.LogInformation("Endpoint");
return "Test of multiple filters";
})
.AddEndpointFilter<AEndpointFilter>()
.AddEndpointFilter<BEndpointFilter>()
.AddEndpointFilter<CEndpointFilter>();
app.Run();Trong code trên, log cho filter và handler thể hiện thứ tự chạy:
AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After nextFilter triển khai interface IEndpointFilter được thể hiện trong ví dụ sau:
namespace Filters.EndpointFilters;
public abstract class ABCEndpointFilters : IEndpointFilter
{
protected readonly ILogger Logger;
private readonly string _methodName;
protected ABCEndpointFilters(ILoggerFactory loggerFactory)
{
Logger = loggerFactory.CreateLogger<ABCEndpointFilters>();
_methodName = GetType().Name;
}
public virtual async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
Logger.LogInformation("{MethodName} Before next", _methodName);
var result = await next(context);
Logger.LogInformation("{MethodName} After next", _methodName);
return result;
}
}
class AEndpointFilter : ABCEndpointFilters
{
public AEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}
class BEndpointFilter : ABCEndpointFilters
{
public BEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}
class CEndpointFilter : ABCEndpointFilters
{
public CEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}Kiểm tra đối tượng với Filter
Xem filter kiểm tra đối tượng Todo sau:
app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilter(async (efiContext, next) =>
{
var tdparam = efiContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(tdparam);
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(efiContext);
});Trong code trên:
- Đối tượng
EndpointFilterInvocationContextcung cấp truy cập vào các tham số liên kết với request cụ thể thông qua phương thứcGetArguments. - Filter được đăng ký dùng
delegatenhận vàoEndpointFilterInvocationContextvà trả vềEndpointFilterDelegate.
Ngoài việc truyền như delegate, filter có thể được đăng ký bằng cách triển khai interface IEndpointFilter. Code sau thể hiện filter trên được đóng gói trong lớp triển khai IEndpointFilter:
public class TodoIsValidFilter : IEndpointFilter
{
private ILogger _logger;
public TodoIsValidFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<TodoIsValidFilter>();
}
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext,
EndpointFilterDelegate next)
{
var todo = efiContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(todo!);
if (!string.IsNullOrEmpty(validationError))
{
_logger.LogWarning(validationError);
return Results.Problem(validationError);
}
return await next(efiContext);
}
}Filter triển khai interface IEndpointFilter có thể resolve (giải quyết) dependencies từ Dependency Injection (DI). Mặc dù filter có thể resolve dependencies từ DI, bản thân filter không thể được resolve từ DI.
ToDoIsValidFilter được áp dụng cho các endpoint sau:
app.MapPut("/todoitems2/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilter<TodoIsValidFilter>();
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
}).AddEndpointFilter<TodoIsValidFilter>();Đăng ký filter dùng endpoint filter factory
Trong một số tình huống, có thể cần cache (bộ nhớ đệm) một số thông tin từ MethodInfo trong filter. Giả sử bạn muốn kiểm tra rằng handler gắn với endpoint filter có tham số đầu tiên là kiểu Todo.
app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilterFactory((filterFactoryContext, next) =>
{
var parameters = filterFactoryContext.MethodInfo.GetParameters();
if (parameters.Length >= 1 && parameters[0].ParameterType == typeof(Todo))
{
return async invocationContext =>
{
var todoParam = invocationContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(todoParam);
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(invocationContext);
};
}
return invocationContext => next(invocationContext);
});Trong code trên:
- Đối tượng
EndpointFilterFactoryContextcung cấp truy cập vàoMethodInfoliên kết với handler của endpoint. - Signature (chữ ký) của handler được kiểm tra bằng cách xem
MethodInfođể tìm signature kiểu mong đợi. Nếu signature mong đợi được tìm thấy, filter kiểm tra được đăng ký vào endpoint. - Nếu signature không khớp, filter pass-through (chuyển tiếp) được đăng ký.
Đăng ký filter trên controller actions
Trong một số tình huống, có thể cần áp dụng cùng logic filter cho cả route-handler based endpoints lẫn controller actions. Với tình huống này, bạn có thể gọi AddEndpointFilter trên ControllerActionEndpointConventionBuilder:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapController()
.AddEndpointFilter(async (efiContext, next) =>
{
efiContext.HttpContext.Items["endpointFilterCalled"] = true;
var result = await next(efiContext);
return result;
});
app.Run();