Nguon: Microsoft Learn · .NET 8.0

Route handlers (bộ xử lý route) trong ứng dụng Minimal API

Nguồn: Route handlers in Minimal API apps

Một WebApplication đã được cấu hình hỗ trợ Map{Verb}MapMethods, trong đó {Verb} là phương thức HTTP viết theo PascalCase như Get, Post, Put hoặc Delete:

csharp
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

csharp
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

csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Instance method

csharp
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

csharp
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

csharp
using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

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

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:

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

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:

csharp
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ả userIdbookId đề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:

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

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

csharp
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 templateURI 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ư RequireAuthorizationWithMetadata để thêm endpoint metadata (siêu dữ liệu).

Ví dụ, đoạn code sau tạo hai nhóm endpoint tương tự:

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

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

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

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

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