Route handlers (bộ xử lý route) trong ứng dụng Minimal API
Một WebApplication đã được cấu hình hỗ trợ Map{Verb} và MapMethods, trong đó {Verb} là phương thức HTTP viết theo PascalCase như Get, Post, Put hoặc Delete:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();Các đối số Delegate (ủy thác) được truyền vào các phương thức này được gọi là route handlers.
Làm việc với Route Handlers
Route handlers là các phương thức thực thi khi route (tuyến đường) khớp. Route handlers có thể là lambda expression (biểu thức lambda), local function (hàm cục bộ), instance method (phương thức của đối tượng) hoặc static method (phương thức tĩnh). Route handlers có thể là đồng bộ hoặc bất đồng bộ.
Các phần sau cung cấp ví dụ về các loại route handler khác nhau.
Lambda expression
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();Local function
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();Instance method
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}Static method
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}Endpoint được định nghĩa ngoài Program.cs
Minimal APIs không cần phải nằm trong file Program.cs. Ví dụ, bạn có thể thiết lập cấu trúc trong file Program.cs và định nghĩa endpoint trong một file riêng:
Program.cs
using MinAPISeparateFile; var builder = WebApplication.CreateSlimBuilder(args); var app = builder.Build(); TodoEndpoints.Map(app); app.Run();
TodoEndpoints.cs
namespace MinAPISeparateFile;
public static class TodoEndpoints
{
public static void Map(WebApplication app)
{
app.MapGet("/", async context =>
{
// Lấy tất cả todo items
await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
});
app.MapGet("/{id}", async context =>
{
// Lấy một todo item
await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
});
}
}Named endpoints (endpoint được đặt tên) và Link Generation (tạo liên kết)
Bạn có thể đặt tên cho endpoint để tạo URL trỏ đến endpoint đó. Việc dùng named endpoint giúp tránh hardcode (mã hóa cứng) đường dẫn trong ứng dụng:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
app.Run();Đoạn code trên hiển thị thông điệp The link to the hello route is /hello từ endpoint /.
Tiêu chí đặt tên endpoint
Tên endpoint phải thỏa mãn các tiêu chí sau:
- Tên endpoint phân biệt chữ hoa/thường.
- Tên endpoint phải là duy nhất toàn cục.
- Tên endpoint được dùng làm OpenAPI operation ID khi hỗ trợ OpenAPI được bật.
Route parameters (tham số route)
Tham số route có thể được capture (bắt) như một phần của định nghĩa route pattern:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();Đoạn code trên trả về thông điệp The user id is 3 and book id is 7 từ URI /users/3/books/7.
Route handler có thể khai báo các tham số để capture. Khi request được gửi đến route có tham số được khai báo, các tham số được phân tích và truyền vào handler. Cách này giúp dễ dàng capture giá trị theo cách type-safe (an toàn về kiểu). Trong code trên, cả userId và bookId đều có kiểu int.
Nếu một trong hai giá trị route không thể chuyển đổi thành int, một exception sẽ được ném ra. Ví dụ: GET request /users/hello/books/3 sẽ ném exception:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Wildcard và Catch-all routes
Route catch-all sau trả về Routing to hello từ endpoint /posts/hello:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();Route constraints (ràng buộc route)
Route constraints giới hạn hành vi khớp của route.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();Bảng sau mô tả các route template trên và hành vi của chúng:
| Route template | URI khớp ví dụ |
|---|---|
/todos/{id:int} | /todos/1 |
/todos/{text} | /todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} | /posts/mypost |
Route groups (nhóm route)
Extension method MapGroup giúp tổ chức các nhóm endpoint với prefix (tiền tố) chung và giảm code lặp lại. Dùng phương thức này để tùy chỉnh toàn bộ nhóm endpoint bằng một lần gọi đến các phương thức như RequireAuthorization và WithMetadata để thêm endpoint metadata (siêu dữ liệu).
Ví dụ, đoạn code sau tạo hai nhóm endpoint tương tự:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Bỏ qua filter nếu phương thức không có tham số TodoDb
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// Chỉ liên quan nếu bạn đang pooling hoặc tái sử dụng instance DbContext
dbContext.IsPrivate = false;
}
};
}public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}Trong kịch bản này, bạn có thể dùng địa chỉ tương đối cho header Location trong kết quả 201 Created:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}Nhóm endpoint đầu tiên chỉ khớp các request có prefix /public/todos và có thể truy cập mà không cần xác thực. Nhóm thứ hai chỉ khớp các request có prefix /private/todos và yêu cầu xác thực.
QueryPrivateTodos là local function sửa đổi các tham số TodoDb của route handler, cho phép chúng truy cập và lưu trữ dữ liệu todo riêng tư.
Route groups cũng hỗ trợ các nested groups (nhóm lồng nhau) và các pattern prefix phức tạp với route parameters và constraints. Trong ví dụ sau, route handler được ánh xạ đến nhóm user có thể capture các route parameter {org} và {group} được định nghĩa trong prefix của outer groups.
Prefix cũng có thể để trống. Cách này hữu ích để thêm endpoint metadata hoặc filters cho một nhóm endpoint mà không thay đổi route pattern.
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");Việc thêm filters hoặc metadata vào một nhóm tương tự như thêm chúng riêng lẻ vào từng endpoint.
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});Trong ví dụ trên, outer filter ghi log request trước inner filter dù outer filter được thêm sau. Vì các filter được áp dụng cho các nhóm khác nhau, thứ tự thêm chúng không quan trọng. Thứ tự thêm filter quan trọng khi áp dụng cho cùng một nhóm hoặc endpoint cụ thể.
Request đến /outer/inner/ ghi log dữ liệu sau:
/outer group filter /inner group filter MapGet filter
Ràng buộc tham số trong route handler
Parameter binding trong ứng dụng Minimal API mô tả chi tiết các quy tắc về cách các tham số route handler được điền giá trị.
Xử lý response từ route handler
Tạo responses trong ứng dụng Minimal API mô tả chi tiết cách các giá trị trả về từ route handlers được chuyển đổi thành responses.