2.3.3 Web API -- 路由与终结点
路由模板
约定路由
特性路由
路由冲突
终结点
ASP.NET Core 中的路由:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/routing?view=aspnetcore-5.0
UseRouting 添加路由中间件到管道,路由中间件用来匹配 url 和具体的 endpoint,然后执行 endpoint
UseEndpoints 添加或者注册 endpoint 到程序中,使得路由中间件可以发现它们
MapRazorPages for Razor Pages 添加所有 Razor Pages 终结点
MapControllers for controllers 添加所有 controller 终结点
MapHub
for SignalR 添加 SignalR 终结点 MapGrpcService
for gRPC 添加 gRPC 终结点
路由模板
路由模板由 token 和其他特定字符组成。比如“/”,特定字符进行路由匹配的时候必须全部匹配
/hello/{name:alpha}
{name:alpha} 是一段 token,一段 token 包括一个参数名,可以跟着一个约束(alpha)或者一个默认值(mingson),比如 {name=mingson} ,或者直接 {name}
app.UseEndpoints(endpoints =>
{
//endpoints.MapControllers();
endpoints.MapGet("/hello/{name:alpha}", async context =>
{
var name = context.Request.RouteValues["name"];
await context.Response.WriteAsync($"Hello {name}!");
});
});
路由模板中的参数被存储在 HttpRequest.RouteValues 中
大小写不敏感
url 中如果有符合,在模板中用{}代替
catch-all 路由模板
在 token 前用 * 或者 ** 加在参数名前,比如 blog/{*slug}
blog/ 后面的字符串会当成 slug 的路由参数值,包括 "/",比如浏览器输入 blog/my/path 会匹配成 foo/my%2Fpath,如果想要得到 blog/my/path 则使用两个 ,foo/{path}
字符串.也是可选的,比如 files/{filename}.{ext?},如果要输入 /files/myFile 也能匹配到这个路由
//app.Run(async context =>
//{
// await context.Response.WriteAsync("my middleware 2");
//});
app.UseEndpoints(endpoints =>
{
//endpoints.MapControllers();
// 将终结点绑定到路由上
endpoints.MapGet("/hello", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
启动程序,访问:https://localhost:5001/hello
输出如下:
my middleware 1Hello World!
获取路由模板参数
endpoints.MapGet("/blog/{*title}", async context =>
{
var title = context.Request.RouteValues["title"];
await context.Response.WriteAsync($"blog title: {title}");
});
启动程序,访问:https://localhost:5001/blog/my-title
输出如下:
my middleware 1blog title: my-title
constraint 约束
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
context =>
{
return context.Response.WriteAsync("inline-constraint match");
});
});
约定路由
默认
endpoints.MapDefaultControllerRoute();
自定义
endpoints.MapControllerRoute("default","{controller=Home}/{action=Index}/{id?}");
// 约定路由
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
// 约定路由也可以同时定义多个
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "blog",
pattern: "blog/{*article}",
defaults: new {controller = "blog", action = "Article"});
});
特性路由
controller
[Route("[controller]")]
http method
[HttpGet("option")]
[HttpGet]
[Route("option")]
[HttpGet]
[Route("option/{id:int}")]
路由冲突
[HttpGet]
//[Route("option")]
public IActionResult GetOption()
{
return Ok(_myOption);
}
如果路由相同,启动程序会报错:
AmbiguousMatchException: The request matched multiple endpoints. Matches:
HelloApi.Controllers.ConfigController.GetOption (HelloApi)
HelloApi.Controllers.ConfigController.GetConfigurations (HelloApi)
终结点
ASP.NET Core 终结点是:
可执行:具有 RequestDelegate。
可扩展:具有元数据集合。
Selectable:可选择性包含路由信息。
可枚举:可通过从 DI 中检索 EndpointDataSource 来列出终结点集合。
终结点可以:
通过匹配 URL 和 HTTP 方法来选择。
通过运行委托来执行。
中间件的每一步都在匹配终结点,所以路由和终结点之间的中间件可以拿到终结点的信息
app.UseRouting();
// 路由和终结点之间的中间件可以拿到终结点的信息
app.Use(next => context =>
{
// 获取当前已经被选择的终结点
var endpoint = context.GetEndpoint();
if (endpoint is null)
{
return Task.CompletedTask;
}
// 输出终结点的名称
Console.WriteLine($"Endpoint: {endpoint.DisplayName}");
// 打印终结点匹配的路由
if (endpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine("Endpoint has route pattern: " +
routeEndpoint.RoutePattern.RawText);
}
// 打印终结点的元数据
foreach (var metadata in endpoint.Metadata)
{
Console.WriteLine($"Endpoint has metadata: {metadata}");
}
return Task.CompletedTask;
});
app.UseEndpoints(endpoints =>
{
//endpoints.MapControllers();
// 将终结点绑定到路由上
endpoints.MapGet("/blog/{title}", async context =>
{
var title = context.Request.RouteValues["title"];
await context.Response.WriteAsync($"blog title: {title}");
});
});
启动程序,访问:https://localhost:5001/blog/my-first-blog
控制台输出如下:
Endpoint: /blog/{title} HTTP: GET
Endpoint has route pattern: /blog/{title}
Endpoint has metadata: System.Runtime.CompilerServices.AsyncStateMachineAttribute
Endpoint has metadata: System.Diagnostics.DebuggerStepThroughAttribute
Endpoint has metadata: Microsoft.AspNetCore.Routing.HttpMethodMetadata
打印 http 方法
// 打印终结点的元数据
foreach (var metadata in endpoint.Metadata)
{
Console.WriteLine($"Endpoint has metadata: {metadata}");
// 打印 http 方法
if (metadata is HttpMethodMetadata httpMethodMetadata)
{
Console.WriteLine($"Current Http Method: {httpMethodMetadata.HttpMethods.FirstOrDefault()}");
}
}
启动程序,访问:https://localhost:5001/blog/my-first-blog
控制台输出如下:
Current Http Method: GET
修改终结点名称、元数据
app.UseEndpoints(endpoints =>
{
//endpoints.MapControllers();
// 将终结点绑定到路由上
endpoints.MapGet("/blog/{title}", async context =>
{
var title = context.Request.RouteValues["title"];
await context.Response.WriteAsync($"blog title: {title}");
}).WithDisplayName("Blog")// 修改名称
.WithMetadata("10001");// 修改元数据
});
调用 UseRouting 之前,终结点始终为 null。
如果找到匹配项,则 UseRouting 和 UseEndpoints 之间的终结点为非 null。
如果找到匹配项,则 UseEndpoints 中间件即为终端。稍后会在本文档中定义终端中间件。
仅当找不到匹配项时才执行 UseEndpoints 后的中间件。
GitHub源码链接:
https://github.com/MINGSON666/Personal-Learning-Library/tree/main/ArchitectTrainingCamp/HelloApi