.NetCore 路由配置指南

ASP.NET Core 控制器使用路由中间件来匹配传入请求的 URL 并将它们映射到操作。本文以使用者的角度,对路由的使用进行概括说明,方便知识回顾与使用。

前言

ASP.NET Core 控制器使用路由中间件来匹配传入请求的 URL 并将它们映射到操作。它支持传统路由,也支持属性路由。如果感觉到陌生,不要着急,继续向下看,下面会一一道来。

传统路由

传统路由通常在 MVC 框架中使用。

Program.cs 配置

它在 program.cs 中定义,如下:

完整方法:

1
2
3
4
5
6
app.MapControllerRoute(
// 路由名称
name: "default",
// 模板为:controllerName/actionName/{id?}
// = 号用于设置默认值
pattern: "{controller=Home}/{action=Index}/{id?}");

简化使用:

1
app.MapDefaultControllerRoute();

说明:

上面的完整路由定义中:

  • 第一个路径段 {controller=Home} 映射到控制器名称。

    UserController 中的控制器名为 User

  • 第二段 {action=Index} 映射到操作名称。

    action 就是 Controller 类中的方法名。

  • 第三段 {id?} 用于可选 id{id?} 中的 ? 使其成为可选。 id 用于映射到模型实体。

Controller 定义

1
2
3
4
5
6
7
8
9
10
public class HomeController : Controller
{
// 默认所有的 http 谓词 get、post 等请求都会调用该接口
public IActionResult Index() {}

// 可以通过特性来约束 http 谓词的调用
// HttpPost 标记后,只有 post 才能调用这个方法
[HttpPost]
public IActionResult Create(){}
}

多个传统路由

可以多次调用 MapControllerRoute 来设置多个传统路由,如下:

1
2
3
4
5
6
app.MapControllerRoute(name: "blog",
pattern: "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });

app.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

上述代码中的 blog 路由是专用的传统路由。 之所以称为专用传统路由是因为 controller 和 action 不会以参数形式出现在路由模板 "blog/{*article}" 中,它们只能具有默认值 { controller = "Blog", action = "Article" }。因此,此路由将会始终映射到操作 BlogController.Article

传统路由顺序

  1. 按定义顺序匹配

  2. 具体的路由在可变路由之前匹配

    比如 users/demo 会在 users/{userId} 之前进行匹配

特性(Attribute)路由

Attribute 本应翻译成属性,但为了与 .NET 中的属性字段区分,本文称之为特性。

特性路由通常在 REST API 中使用。

Program.cs 配置

1
2
3
4
5
6
7
8
9
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();

app.MapControllers();

app.Run();

属性路由通过调用 MapControllers 来映射属性路由控制器。

Controller 定义

下面的示例中,HomeController 匹配一组类似于默认传统路由 {controller=Home}/{action=Index}/{id?} 匹配的 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HomeController : Controller
{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
[Route("Home/Index/{id?}")]
public IActionResult Index(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[Route("Home/About")]
[Route("Home/About/{id?}")]
public IActionResult About(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

保留关键字

  • action
  • area
  • controller
  • handler
  • page

这些关键词是保留的路由参数名,在定义路由时,不能使用这些关键词。

HTTP 谓词模板

路由模板

路由模板用于定义路由匹配的模板,它分为

谓词模板示例

假设如下控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
[HttpGet] // GET /api/test2
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpGet("{id}")] // GET /api/test2/xyz
public IActionResult GetProduct(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[HttpGet("int/{id:int}")] // GET /api/test2/int/3
public IActionResult GetIntProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[HttpGet("int2/{id}")] // GET /api/test2/int2/3
public IActionResult GetInt2Product(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

在上述代码中:

  • 每个操作都包含 [HttpGet] 属性,该属性仅将匹配限制为 HTTP GET 请求。

  • GetProduct 操作包含 "{id}" 模板,因此 id 被附加到控制器上的 "api/[controller]" 模板中。 方法模板为 "api/[controller]/"{id}""。 因此,此操作仅匹配 /api/test2/xyz/api/test2/123/api/test2/{any string} 等形式的 GET 请求。

    1
    2
    3
    4
    5
    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
    return ControllerContext.MyDisplayRouteInfo(id);
    }
  • GetIntProduct 操作包含 "int/{id:int}") 模板。 模板的 :int 部分将 id 路由值限制为可以转换为整数的字符串。

    1
    2
    3
    4
    5
    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
    return ControllerContext.MyDisplayRouteInfo(id);
    }

    对于 /api/test2/int/abc 的 GET 请求,将会无法匹配到路由,并返回 404 Not Found 错误

  • GetInt2Product 操作在模板中包含 {id},但不将 id 限制为可以转换为整数的值。 对于 /api/test2/int2/abc 的 GET 请求,处理如下:

    1. 与此路由匹配。

    2. 模型绑定无法将 abc 转换为整数。 该方法的 id 参数是整数。

    3. 返回 400 Bad Request,因为模型绑定未能将 abc 转换为整数。

    1
    2
    3
    4
    5
    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
    return ControllerContext.MyDisplayRouteInfo(id);
    }

生成 REST API 时,很少需要在 操作方法 上使用 [Route(...)] ,因为该操作接受所有 HTTP 方法。 建议使用更具体的 HTTP 谓词属性来明确 API 所支持的操作。 API 的 REST 客户端应知道哪些路径和 HTTP 谓词映射到特定的逻辑操作。

REST API 应使用属性路由将应用的功能建模为一组资源,其中操作由 HTTP 谓词表示。 也就是说,对同一逻辑资源执行的许多操作(例如,GET 和 POST)都使用相同 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[ApiController]
public class MyProductsController : ControllerBase
{
[HttpGet("/products3")]
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpPost("/products3")]
public IActionResult CreateProduct(MyProduct myProduct)
{
return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
}
}

上述代码中的 URL 路径为 /products3

  • HTTP 谓词GET 时,调用 MyProductsController.ListProducts
  • HTTP 谓词POST 时,调用 MyProductsController.CreateProduct

特性路由组合

控制器上定义的所有路由模板均作为操作上路由模板的前缀。在控制器上放置的路由特性会使控制器中的所有操作都使用该特性路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ApiController]
[Route("products")]
public class ProductsApiController : ControllerBase
{
[HttpGet]
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

在上面的示例中:

  • URL 路径 /products 可以匹配 ProductsApi.ListProducts
  • URL 路径 /products/5 可以匹配 ProductsApi.GetProduct(int)

这两项操作仅匹配 HTTP GET,因为它们标记了 [HttpGet]

操作上以 /~/ 开头的路由模板不与控制器的路由模板合并。

Attribute [Route("Home")] 结合 定义路由模板
[Route("")] "Home"
[Route("Index")] "Home/Index"
[Route("/")] ""
[Route("About")] "Home/About"

特性路由继承

在父类控制器上定义的路由特性会继承给子类,可以在父类中定义一个通用的路由特性,减少在子类的控制器上重复定义。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
[ApiController]
[Route("[controller]/[action]")]
public abstrct class CustomBaseController : ControllerBase
{
}

// 此处可不进行路由特性定义
public class ProductsApiController : CustomBaseController
{
[HttpGet]
public IActionResult Index(){}
}

标记替换

特性路由支持标记替换,将标记用方括号([])括起来即可。 标记 [action][area][controller] 会替换成定义了路由的操作中的操作名称、区域名称和控制器名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Products0:控制器名称
[Route("[controller]/[action]")]
public class Products0Controller : Controller
{
// List:操作名称
[HttpGet]
public IActionResult List()
{
return ControllerContext.MyDisplayRouteInfo();
}


[HttpGet("{id}")]
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

Areas是一项 MVC 功能,用于将相关功能作为一个单独的组组织到一个组中,单击链接可跳转阅读更加详细的内容

标记样式转换

[controller][action] 等会默认使用定义的名称作用 URL,而在实际开发中,我们可能需要将 PascalCase 命名转换成 hyphenCase 命名,如将 FindAll 变成 find-all

可以通过实现 IOutboundParameterTransformer 接口来自定义。

接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Text.RegularExpressions;

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

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

使用:

1
2
3
4
5
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});

RouteTokenTransformerConvention 是应用程序的模型约定,可以:

  1. 将参数转换程序应用到程序中的所有特性路由中。

  2. 在替换特性路由标记值时对其进行自定义

多个路由特性

同一个控制器或者路由上,可以同时添加多个路由特性标记。

1
2
3
4
5
6
7
8
9
10
11
[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
[HttpPost("Buy")] // Matches 'Products6/Buy' and 'Store/Buy'
[HttpPost("Checkout")] // Matches 'Products6/Checkout' and 'Store/Checkout'
public IActionResult Buy()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

一般不要使用多个路由特性,会让 URL 看起来不易于理解,且容易冲突。

可选参数、默认值和约束

特性路由支持使用与传统路由相同的内联语法,来指定可选参数、默认值和约束。

1
2
3
4
5
6
7
8
public class Products14Controller : Controller
{
[HttpPost("{controller=ProductsDefault}/{id:int:string}/{name?}")]
public IActionResult ShowProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

使用说明:

  1. = 赋予默认值
  2. : 进行约束,可以同时使用多个约束
  3. ? 表示可选参数

内置路由约束:

约束 示例 匹配项示例 说明
int {id:int} 123456789, -123456789 匹配任何整数
bool {active:bool} true, FALSE 匹配 truefalse。 不区分大小写
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm 在固定区域性中匹配有效的 DateTime 值。 请参阅前面的警告。
decimal {price:decimal} 49.99, -1,000.01 在固定区域性中匹配有效的 decimal 值。 请参阅前面的警告。
double {weight:double} 1.234, -1,001.01e8 在固定区域性中匹配有效的 double 值。 请参阅前面的警告。
float {weight:float} 1.234, -1,001.01e8 在固定区域性中匹配有效的 float 值。 请参阅前面的警告。
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 匹配有效的 Guid
long {ticks:long} 123456789, -123456789 匹配有效的 long
minlength(value) {username:minlength(4)} Rick 字符串必须至少为 4 个字符
maxlength(value) {filename:maxlength(8)} MyFile 字符串不得超过 8 个字符
length(length) {filename:length(12)} somefile.txt 字符串必须正好为 12 个字符
length(min,max) {filename:length(8,16)} somefile.txt 字符串必须至少为 8 个字符,且不得超过 16 个字符
min(value) {age:min(18)} 19 整数值必须至少为 18
max(value) {age:max(120)} 91 整数值不得超过 120
range(min,max) {age:range(18,120)} 91 整数值必须至少为 18,且不得超过 120
alpha {name:alpha} Rick 字符串必须由一个或多个字母字符组成,a-z,并区分大小写。
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 字符串必须与正则表达式匹配。 请参阅有关定义正则表达式的提示。
required {name:required} Rick 用于强制在 URL 生成过程中存在非参数值

自定义特性路由

所有路由属性都实现 IRouteTemplateProvider。 ASP.NET Core 运行时:

  1. 应用启动时,在控制器类和操作方法上查找属性。
  2. 使用实现 IRouteTemplateProvider 的属性来构建初始路由集。

每个 IRouteTemplateProvider 都允许定义一个包含自定义路由模板、顺序和名称的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
{
public string Template => "api/[controller]";
public int? Order => 2;
public string Name { get; set; } = string.Empty;
}

[MyApiController]
[ApiController]
public class MyTestApiController : ControllerBase
{
// GET /api/MyTestApi
[HttpGet]
public IActionResult Get()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

上述 Get 方法返回 Order = 2, Template = api/MyTestApi

路由返回值

ASP.NET Core 使用以下类型作为 Web API 控制器的操作返回类型:

请点击 ASP.NET Core Web API 中控制器操作的返回类型 进行详细阅读

传统路由与特性路由对比

类型 传统路由 特性路由
定义方式 Program.cs 中调用 MapControllerRoute 建立 URL 映射 在每个 Controller 中通过特性来定义 URL 映射
操作性 更简洁 要对每个 action 进行定义

路由快速配置

当理解了路由相关知识后,需要可以快速应用到实际项目中,本节记录一些快速配置代码,方便进行初始化。

映射路由

1
2
3
4
// ...
// 在 Run 之前调用 MapControllers 进行映射
app.MapControllers();
app.Run();

设置 hyphenCase 路由

增加 SlugifyParameterTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Text.RegularExpressions;

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

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

Program.cs 中配置

1
2
3
4
5
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});

新建路由基类

所有子类都继承自这个基类

1
2
3
4
5
[Route("api/v1/[controller]")]
[ApiController]
public class CustomControllerBase: ControllerBase
{
}

参考

在 ASP.NET Core 中路由到控制器操作 | Microsoft Learn

ASP.NET Core 中的路由 | Microsoft Learn

ASP.NET Core Web API 中控制器操作的返回类型

ASP.NET Core 中的模型绑定 | Microsoft Learn