ASP.NET Core 控制器使用路由中间件来匹配传入请求的 URL
并将它们映射到操作。本文以使用者的角度,对路由的使用进行概括说明,方便知识回顾与使用。
前言
ASP.NET Core 控制器使用路由中间件来匹配传入请求的 URL
并将它们映射到操作。它支持传统路由,也支持属性路由。如果感觉到陌生,不要着急,继续向下看,下面会一一道来。
传统路由
传统路由通常在 MVC 框架中使用。
Program.cs 配置
它在 program.cs
中定义,如下:
完整方法:
1 2 3 4 5 6 app.MapControllerRoute( name: "default" , 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 { public IActionResult Index () {} [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
。
传统路由顺序
按定义顺序匹配
具体的路由在可变路由之前匹配
比如 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 ] public IActionResult ListProducts () { return ControllerContext.MyDisplayRouteInfo(); } [HttpGet("{id}" ) ] public IActionResult GetProduct (string id ) { return ControllerContext.MyDisplayRouteInfo(id); } [HttpGet("int/{id:int}" ) ] public IActionResult GetIntProduct (int id ) { return ControllerContext.MyDisplayRouteInfo(id); } [HttpGet("int2/{id}" ) ] 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}" ) ] public IActionResult GetProduct (string id ){ return ControllerContext.MyDisplayRouteInfo(id); }
GetIntProduct
操作包含 "int/{id:int}")
模板。 模板的 :int
部分将 id
路由值限制为可以转换为整数的字符串。
1 2 3 4 5 [HttpGet("int/{id:int}" ) ] 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 请求,处理如下:
与此路由匹配。
模型绑定无法将 abc
转换为整数。 该方法的
id
参数是整数。
返回 400 Bad
Request ,因为模型绑定未能将 abc
转换为整数。
1 2 3 4 5 [HttpGet("int2/{id}" ) ] 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]
。
操作上以 /
或 ~/
开头的路由模板不与控制器的路由模板合并。
[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 [Route("[controller]/[action]" ) ] public class Products0Controller : Controller { [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 3 4 5 6 7 8 9 10 11 [Route("Store" ) ] [Route("[controller]" ) ] public class Products6Controller : Controller { [HttpPost("Buy" ) ] [HttpPost("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); } }
使用说明:
=
赋予默认值
:
进行约束,可以同时使用多个约束
?
表示可选参数
内置路由约束:
int
{id:int}
123456789
,
-123456789
匹配任何整数
bool
{active:bool}
true
, FALSE
匹配 true
或
false
。 不区分大小写
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
运行时:
应用启动时,在控制器类和操作方法上查找属性。
使用实现 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 { [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 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