Nguon: Microsoft Learn · .NET 8.0

Routing (Định tuyến) trong ASP.NET Core

Nguồn: Routing in ASP.NET Core

Tổng quan

Routing (định tuyến) chịu trách nhiệm khớp các incoming HTTP request và điều phối các request đó đến các executable endpoint của ứng dụng. Endpoint là các đơn vị code xử lý request có thể thực thi của ứng dụng. Endpoint được định nghĩa trong ứng dụng và được cấu hình khi ứng dụng khởi động. Quá trình khớp endpoint có thể trích xuất các giá trị từ URL của request và cung cấp các giá trị đó cho quá trình xử lý request. Sử dụng thông tin endpoint từ ứng dụng, routing cũng có thể tạo URL ánh xạ đến các endpoint.

Ứng dụng có thể cấu hình routing bằng cách sử dụng:

Cơ bản về Routing

Code sau đây hiển thị ví dụ cơ bản về routing:

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

app.MapGet("/", () => "Hello World!");

app.Run();

Ví dụ trên bao gồm một endpoint duy nhất sử dụng phương thức MapGet:

Routing sử dụng một cặp middleware, được đăng ký bởi UseRoutingUseEndpoints:

Ứng dụng thường không cần gọi UseRouting hoặc UseEndpoints. WebApplicationBuilder cấu hình middleware pipeline bọc middleware được thêm vào Program.cs với UseRoutingUseEndpoints. Tuy nhiên, ứng dụng có thể thay đổi thứ tự UseRoutingUseEndpoints chạy bằng cách gọi các phương thức này một cách tường minh.

Ví dụ với kiểm soát routing tường minh:

csharp
app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

Trong code trên:

Lưu ý: Route được thêm trực tiếp vào WebApplication thực thi ở cuối pipeline.

Endpoints

Phương thức MapGet được sử dụng để định nghĩa một endpoint. Endpoint là thứ có thể:

Các phương thức bổ sung có thể được sử dụng để kết nối các tính năng framework ASP.NET Core với hệ thống routing:

Ví dụ sau hiển thị routing với route template phức tạp hơn:

csharp
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

Chuỗi /hello/{name:alpha} là một route template. Route template được sử dụng để cấu hình cách endpoint được khớp. Trong trường hợp này, template khớp:

Đoạn thứ hai của URL path, {name:alpha}:

Ví dụ với Health Checks và Authorization

Ví dụ sau hiển thị routing với health checks và authorization (ủy quyền):

csharp
app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

Ví dụ trên minh họa cách:

Endpoint Metadata (Siêu dữ liệu endpoint)

Trong ví dụ trên, có hai endpoint, nhưng chỉ có health check endpoint được đính kèm authorization policy. Nếu request khớp với health check endpoint, /healthz, kiểm tra authorization được thực hiện. Điều này minh họa rằng các endpoint có thể có dữ liệu thêm được đính kèm. Dữ liệu thêm này được gọi là metadata (siêu dữ liệu) của endpoint:

Khái niệm Routing

Định nghĩa ASP.NET Core Endpoint

Một ASP.NET Core endpoint là:

Code sau hiển thị cách lấy và kiểm tra endpoint khớp với request hiện tại:

csharp
app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

Vị trí middleware trong pipeline

Code sau minh họa rằng, tùy thuộc vào vị trí gọi app.Use trong pipeline, có thể không có endpoint:

csharp
// Vị trí 1: trước khi routing chạy, endpoint luôn null ở đây.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Vị trí 2: sau khi routing chạy, endpoint sẽ không null nếu routing tìm thấy kết quả khớp.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Vị trí 3: chạy khi endpoint này khớp
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Vị trí 4: chạy sau UseEndpoints - chỉ chạy nếu không có kết quả khớp.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

Chạy code này với URL / hiển thị:

code
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Đầu ra này minh họa rằng:

Terminal Middleware so với Routing

Code sau minh họa cả terminal middleware và routing:

csharp
// Cách 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Cách 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Danh sách so sánh terminal middleware với routing:

URL Matching (Khớp URL)

URL matching:

Route Template Precedence (Ưu tiên route template)

Route template precedence là hệ thống gán giá trị cho mỗi route template dựa trên mức độ cụ thể của nó:

URL Generation (Tạo URL)

URL generation:

Endpoint routing bao gồm LinkGenerator API. LinkGenerator là singleton service có thể truy cập từ DI. LinkGenerator API có thể được sử dụng bên ngoài context của request đang thực thi.

Link generator cung cấp các extension method sau:

Middleware Example với LinkGenerator

Trong ví dụ sau, middleware sử dụng LinkGenerator API để tạo link đến action method liệt kê sản phẩm của cửa hàng:

csharp
public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}

Route Templates

Các token trong {} xác định các route parameter được liên kết nếu route được khớp. Nhiều route parameter có thể được định nghĩa trong một route segment, nhưng các route parameter phải được phân tách bằng một literal value.

Literal text ngoài route parameter (ví dụ: {id}) và path separator / phải khớp với text trong URL. Text matching không phân biệt chữ hoa chữ thường và dựa trên biểu diễn được decode của URL path.

Catch-all Parameters

Dấu hoa thị * hoặc ** kép:

Optional Route Parameters và Default Values

Route parameter có thể có default values (giá trị mặc định) được chỉ định bằng cách chỉ định giá trị mặc định sau tên parameter được phân tách bằng dấu bằng (=). Ví dụ, {controller=Home} định nghĩa Home là giá trị mặc định cho controller. Route parameter được làm optional bằng cách thêm dấu hỏi (?) vào cuối tên parameter. Ví dụ, id?.

Route Constraints và Inline Constraints

Route parameter có thể có các constraint phải khớp với route value được liên kết từ URL.

Cảnh báo: Không sử dụng constraint cho input validation. Nếu constraint được sử dụng cho input validation, input không hợp lệ dẫn đến response 404 Not Found. Input không hợp lệ nên tạo ra 400 Bad Request với thông báo lỗi phù hợp. Route constraint được sử dụng để phân biệt các route tương tự, không phải để validate input cho một route cụ thể.

Default Route Constraints

Bảng sau minh họa các default route constraint và hành vi dự kiến của chúng:

ConstraintVí dụVí dụ khớpGhi chú
int{id:int}123456789, -123456789Khớp bất kỳ số nguyên nào
bool{active:bool}true, FALSEKhớp true hoặc false. Không phân biệt chữ hoa/thường
datetime{dob:datetime}2016-12-31, 2016-12-31 7:32pmKhớp giá trị DateTime hợp lệ trong invariant culture
decimal{price:decimal}49.99, -1,000.01Khớp giá trị decimal hợp lệ trong invariant culture
double{weight:double}1.234, -1,001.01e8Khớp giá trị double hợp lệ trong invariant culture
float{weight:float}1.234, -1,001.01e8Khớp giá trị float hợp lệ trong invariant culture
guid{id:guid}CD2C1638-1638-72D5-1638-DEADBEEF1638Khớp giá trị Guid hợp lệ
long{ticks:long}123456789, -123456789Khớp giá trị long hợp lệ
minlength(value){username:minlength(4)}RickChuỗi phải có ít nhất 4 ký tự
maxlength(value){filename:maxlength(8)}MyFileChuỗi không được nhiều hơn 8 ký tự
length(length){filename:length(12)}somefile.txtChuỗi phải có đúng 12 ký tự
length(min,max){filename:length(8,16)}somefile.txtChuỗi phải có ít nhất 8 và không nhiều hơn 16 ký tự
min(value){age:min(18)}19Giá trị số nguyên phải ít nhất là 18
max(value){age:max(120)}91Giá trị số nguyên không được nhiều hơn 120
range(min,max){age:range(18,120)}91Giá trị số nguyên phải ít nhất là 18 nhưng không nhiều hơn 120
alpha{name:alpha}RickChuỗi phải gồm một hoặc nhiều ký tự chữ cái, a-z và không phân biệt chữ hoa/thường
regex(expression){ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}123-45-6789Chuỗi phải khớp với biểu thức chính quy
required{name:required}RickBắt buộc phải có giá trị tường minh (không phải ambient value) trong quá trình tạo URL
file{filename:file}myfile.txtChuỗi có thể chứa các path segment, nhưng segment cuối cùng phải có dấu chấm (.) và theo sau là một hoặc nhiều ký tự không phải dấu chấm
nonfile{page:nonfile}PageNameChuỗi không được có dấu chấm trong path segment cuối cùng được theo sau bởi một hoặc nhiều ký tự không phải dấu chấm

Custom Route Constraints (Constraint tùy chỉnh)

Custom route constraint có thể được tạo bằng cách implement interface IRouteConstraint. Interface IRouteConstraint chứa Match, trả về true nếu constraint được thỏa mãn và false ngược lại.

Để sử dụng custom IRouteConstraint, loại route constraint phải được đăng ký với ConstraintMap của ứng dụng trong service container:

csharp
builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

Parameter Transformers

Parameter transformer:

Ví dụ, một parameter transformer tùy chỉnh slugify trong route pattern blog\{article:slugify} với Url.Action(new { article = "MyTestArticle" }) tạo ra blog\my-test-article:

csharp
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}

Route Groups

Extension method MapGroup giúp tổ chức các nhóm endpoint với common prefix (tiền tố chung) và giảm code lặp lại. Sử dụng phương thức này để tùy chỉnh toàn bộ nhóm endpoint với một lần gọi các phương thức như RequireAuthorizationWithMetadata.

Ví dụ, 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();
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;
}

Route group cũng hỗ trợ các nhóm lồng nhau và các complex prefix pattern với route parameter và constraint.

Short-Circuit Middleware After Routing (Ngắn mạch Middleware sau Routing)

Khi routing khớp một endpoint, nó thường để phần còn lại của middleware pipeline chạy trước khi gọi endpoint logic. Service có thể giảm việc sử dụng tài nguyên bằng cách lọc ra các request đã biết sớm trong pipeline. Sử dụng extension method ShortCircuit để khiến routing gọi endpoint logic ngay lập tức và sau đó kết thúc request:

csharp
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();

Sử dụng phương thức MapShortCircuit để thiết lập short-circuiting cho nhiều route cùng lúc:

csharp
app.MapShortCircuit(404, "robots.txt", "favicon.ico");

Hướng dẫn hiệu suất cho Routing

Khi ứng dụng có vấn đề hiệu suất, routing thường bị nghi ngờ là nguyên nhân. Nguyên nhân gốc rễ phổ biến nhất của hiệu suất routing chậm thường là do middleware tùy chỉnh hoạt động không tốt.

Code mẫu sau minh họa kỹ thuật cơ bản để xác định nguồn gốc của độ trễ:

csharp
var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");

Các tính năng routing tiềm ẩn tốn kém

Danh sách sau cung cấp một số thông tin về các tính năng routing tương đối tốn kém so với route template cơ bản:

Host Matching với RequireHost

RequireHost áp dụng constraint cho route yêu cầu host được chỉ định:

csharp
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

Cảnh báo: API phụ thuộc vào Host header, chẳng hạn như HttpRequest.HostRequireHost, có thể bị giả mạo bởi client. Để ngăn chặn host và port spoofing, sử dụng một trong các cách tiếp cận sau: