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:
- Controllers
- Razor Pages
- SignalR
- gRPC Services
- Endpoint-enabled middleware (middleware hỗ trợ endpoint) như Health Checks
- Delegate và lambda được đăng ký với routing
Cơ bản về Routing
Code sau đây hiển thị ví dụ cơ bản về routing:
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:
- Khi HTTP
GETrequest được gửi đến root URL/: - Request delegate thực thi.
Hello World!được ghi vào HTTP response.- Nếu phương thức request không phải là
GEThoặc root URL không phải là/, không có route nào khớp và HTTP 404 được trả về.
Routing sử dụng một cặp middleware, được đăng ký bởi UseRouting và UseEndpoints:
UseRoutingthêm route matching (khớp route) vào middleware pipeline. Middleware này nhìn vào tập hợp các endpoint được định nghĩa trong ứng dụng và chọn kết quả khớp tốt nhất dựa trên request.UseEndpointsthêm endpoint execution (thực thi endpoint) vào middleware pipeline. Nó chạy delegate liên kết với endpoint được chọn.
Ứ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 UseRouting và UseEndpoints. Tuy nhiên, ứng dụng có thể thay đổi thứ tự UseRouting và UseEndpoints 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:
app.Use(async (context, next) =>
{
// ...
await next(context);
});
app.UseRouting();
app.MapGet("/", () => "Hello World!");Trong code trên:
- Lời gọi
app.Useđăng ký middleware tùy chỉnh chạy ở đầu pipeline. - Lời gọi
UseRoutingcấu hình route matching middleware để chạy sau middleware tùy chỉnh. - Endpoint được đăng ký với
MapGetchạy ở cuối pipeline.
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 chọn, bằng cách khớp URL và HTTP method.
- Được thực thi, bằng cách chạy delegate.
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:
MapRazorPagescho Razor PagesMapControllerscho controllersMapHub<THub>cho SignalRMapGrpcService<TService>cho gRPC
Ví dụ sau hiển thị routing với route template phức tạp hơn:
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:
- URL như
/hello/Docs - Bất kỳ URL path nào bắt đầu bằng
/hello/theo sau là một chuỗi ký tự chữ cái.:alphaáp dụng route constraint chỉ khớp với ký tự chữ cái.
Đoạn thứ hai của URL path, {name:alpha}:
- Được liên kết với tham số
name. - Được capture và lưu trữ trong
HttpRequest.RouteValues.
Ví dụ với Health Checks và Authorization
Ví dụ sau hiển thị routing với health checks và authorization (ủy quyền):
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");Ví dụ trên minh họa cách:
- Authorization middleware có thể được sử dụng với routing.
- Endpoint có thể được sử dụng để cấu hình hành vi authorization.
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:
- Metadata có thể được xử lý bởi routing-aware middleware.
- Metadata có thể là bất kỳ kiểu .NET nào.
Khái niệm Routing
Định nghĩa ASP.NET Core Endpoint
Một ASP.NET Core endpoint là:
- Executable (Có thể thực thi): Có một
RequestDelegate. - Extensible (Có thể mở rộng): Có một
Metadatacollection. - Selectable (Có thể chọn): Tùy chọn, có thông tin routing.
- Enumerable (Có thể liệt kê): Tập hợp các endpoint có thể được liệt kê bằng cách lấy
EndpointDataSourcetừ DI.
Code sau hiển thị cách lấy và kiểm tra endpoint khớp với request hiện tại:
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:
// 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ị:
1. Endpoint: (null) 2. Endpoint: Hello 3. Endpoint: Hello
Đầu ra này minh họa rằng:
- Endpoint luôn null trước khi
UseRoutingđược gọi. - Nếu tìm thấy kết quả khớp, endpoint không null giữa
UseRoutingvàUseEndpoints. - Middleware
UseEndpointslà terminal (cuối cùng) khi tìm thấy kết quả khớp. - Middleware sau
UseEndpointschỉ thực thi khi không tìm thấy kết quả khớp.
Terminal Middleware so với Routing
Code sau minh họa cả terminal middleware và routing:
// 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:
- Cả hai cách đều cho phép kết thúc processing pipeline.
- Terminal middleware cho phép định vị middleware ở bất kỳ vị trí nào tùy ý trong pipeline.
- Endpoint thực thi tại vị trí của
UseEndpoints. - Terminal middleware cho phép code tùy ý xác định khi nào middleware khớp.
- Endpoint giao tiếp với middleware như
UseAuthorizationvàUseCors.
URL Matching (Khớp URL)
URL matching:
- Là quá trình routing khớp incoming request với một endpoint.
- Dựa trên dữ liệu trong URL path và headers.
- Có thể được mở rộng để xem xét bất kỳ dữ liệu nào trong request.
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ó:
- Templates có nhiều segment hơn được coi là cụ thể hơn.
- Segment có literal text được coi là cụ thể hơn segment tham số.
- Segment tham số có constraint được coi là cụ thể hơn segment không có constraint.
- Complex segment được coi là cụ thể như segment tham số có constraint.
- Catch-all parameters là ít cụ thể nhất.
URL Generation (Tạo URL)
URL generation:
- Là quá trình routing có thể tạo URL path dựa trên một tập hợp route values.
- Cho phép tách biệt logic giữa endpoint và URL truy cập chúng.
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:
GetPathByActionGetUriByActionGetPathByPageGetUriByPage
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:
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:
- Có thể được sử dụng như prefix cho route parameter để liên kết với phần còn lại của URI.
- Được gọi là catch-all parameters. Ví dụ,
blog/{**slug}: - Khớp với bất kỳ URI nào bắt đầu bằng
blog/và có bất kỳ giá trị nào theo sau. - Giá trị theo sau
blog/được gán cho slug route value.
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:
| Constraint | Ví dụ | Ví dụ khớp | Ghi chú |
|---|---|---|---|
int | {id:int} | 123456789, -123456789 | Khớp bất kỳ số nguyên nào |
bool | {active:bool} | true, FALSE | Khớ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:32pm | Khớp giá trị DateTime hợp lệ trong invariant culture |
decimal | {price:decimal} | 49.99, -1,000.01 | Khớp giá trị decimal hợp lệ trong invariant culture |
double | {weight:double} | 1.234, -1,001.01e8 | Khớp giá trị double hợp lệ trong invariant culture |
float | {weight:float} | 1.234, -1,001.01e8 | Khớp giá trị float hợp lệ trong invariant culture |
guid | {id:guid} | CD2C1638-1638-72D5-1638-DEADBEEF1638 | Khớp giá trị Guid hợp lệ |
long | {ticks:long} | 123456789, -123456789 | Khớp giá trị long hợp lệ |
minlength(value) | {username:minlength(4)} | Rick | Chuỗi phải có ít nhất 4 ký tự |
maxlength(value) | {filename:maxlength(8)} | MyFile | Chuỗi không được nhiều hơn 8 ký tự |
length(length) | {filename:length(12)} | somefile.txt | Chuỗi phải có đúng 12 ký tự |
length(min,max) | {filename:length(8,16)} | somefile.txt | Chuỗi phải có ít nhất 8 và không nhiều hơn 16 ký tự |
min(value) | {age:min(18)} | 19 | Giá trị số nguyên phải ít nhất là 18 |
max(value) | {age:max(120)} | 91 | Giá trị số nguyên không được nhiều hơn 120 |
range(min,max) | {age:range(18,120)} | 91 | Giá trị số nguyên phải ít nhất là 18 nhưng không nhiều hơn 120 |
alpha | {name:alpha} | Rick | Chuỗ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-6789 | Chuỗi phải khớp với biểu thức chính quy |
required | {name:required} | Rick | Bắ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.txt | Chuỗ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} | PageName | Chuỗ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:
builder.Services.AddRouting(options =>
options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));Parameter Transformers
Parameter transformer:
- Thực thi khi tạo link bằng cách sử dụng
LinkGenerator. - Implement
Microsoft.AspNetCore.Routing.IOutboundParameterTransformer. - Được cấu hình bằng cách sử dụng
ConstraintMap. - Lấy route value của parameter và chuyển đổi nó thành giá trị chuỗi mới.
- Kết quả là sử dụng giá trị đã chuyển đổi trong link được tạo.
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:
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ư RequireAuthorization và WithMetadata.
Ví dụ, 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();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:
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:
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ễ:
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:
- Regular expressions (Biểu thức chính quy): Có thể viết biểu thức chính quy phức tạp, hoặc có thời gian chạy lâu với lượng input nhỏ.
- Complex segments (
{x}-{y}-{z}): Đắt hơn đáng kể so với phân tích một URL path segment thông thường. - Synchronous data access (Truy cập dữ liệu đồng bộ): Nhiều ứng dụng phức tạp có database access như một phần của routing.
Host Matching với RequireHost
RequireHost áp dụng constraint cho route yêu cầu host được chỉ định:
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.Host và RequireHost, 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:
- Sử dụng
HttpContext.Connection(ConnectionInfo.LocalPort) nơi các port được kiểm tra. - Sử dụng Host filtering.