# 模块二 基础巩固 路由与终结点

## 2.3.3 Web API -- 路由与终结点 <a href="#id-233webapi-lu-you-yu-zhong-jie-dian" id="id-233webapi-lu-you-yu-zhong-jie-dian"></a>

* 路由模板
* 约定路由
* 特性路由
* 路由冲突
* 终结点

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 终结点

### 路由模板 <a href="#lu-you-mo-ban" id="lu-you-mo-ban"></a>

路由模板由 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 路由模板 <a href="#catchall-lu-you-mo-ban" id="catchall-lu-you-mo-ban"></a>

* 在 token 前用 \* 或者 \*\* 加在参数名前，比如 blog/{\*slug}
* blog/ 后面的字符串会当成 slug 的路由参数值，包括 "/"，比如浏览器输入 blog/my/path 会匹配成 foo/my%2Fpath，如果想要得到 blog/my/path 则使用两个 **，foo/{**&#x70;ath}
* 字符串.也是可选的，比如 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 约束 <a href="#constraint-yue-shu" id="constraint-yue-shu"></a>

![](https://3083743005-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F8gwpNo3eyzHkX0O40HRA%2Fuploads%2F6e11M4dMmJHL9CC0eVIp%2F189.jpg?alt=media\&token=1ba4efcf-0069-43cd-8e09-b0f9d77b91ea)

![](https://3083743005-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F8gwpNo3eyzHkX0O40HRA%2Fuploads%2FXylzEkAgtu4JsZmUxmup%2F190.jpg?alt=media\&token=fb3d2d67-e625-49b6-a3e4-e061633ef334)

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

### 约定路由 <a href="#yue-ding-lu-you" id="yue-ding-lu-you"></a>

默认

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

### 特性路由 <a href="#te-xing-lu-you" id="te-xing-lu-you"></a>

controller

```
[Route("[controller]")]
```

http method

```
[HttpGet("option")]

[HttpGet]
[Route("option")]

[HttpGet]
[Route("option/{id:int}")]
```

### 路由冲突 <a href="#lu-you-chong-tu" id="lu-you-chong-tu"></a>

```
[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)
```

### 终结点 <a href="#zhong-jie-dian" id="zhong-jie-dian"></a>

ASP.NET Core 终结点是：

* 可执行：具有 RequestDelegate。
* 可扩展：具有元数据集合。
* Selectable:可选择性包含路由信息。
* 可枚举：可通过从 DI 中检索 EndpointDataSource 来列出终结点集合。

终结点可以：

* 通过匹配 URL 和 HTTP 方法来选择。
* 通过运行委托来执行。

![](https://3083743005-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F8gwpNo3eyzHkX0O40HRA%2Fuploads%2FWgTVPpIv1TTUpJPz9NMq%2F191.jpg?alt=media\&token=4769ee52-f2de-4b30-8608-6af7fc167905)

中间件的每一步都在匹配终结点，所以路由和终结点之间的中间件可以拿到终结点的信息

```
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源码链接： <a href="#github-yuan-ma-lian-jie" id="github-yuan-ma-lian-jie"></a>

<https://github.com/MingsonZheng/ArchitectTrainingCamp/tree/main/HelloApi>
