3. 路由与端点:路由的概念、传统路由与属性路由、路由约束、URL生成
好,咱们今天聊聊路由。说实话,路由是 ASP.NET Core 的骨架。你想想看,用户敲个网址进来,服务器怎么知道该让哪段代码去处理?这就是路由干的事。
我刚开始学的时候,觉得路由不就是个地址映射嘛,有啥好学的?后来在项目中踩过坑才明白——路由设计得好,项目后期维护能省一半力气。嗯,咱们一步步来。
3.1 路由到底是什么?
路由,说白了就是一套规则。它告诉 ASP.NET Core:当用户访问某个 URL 时,应该调用哪个 Controller 的哪个 Action 方法。
举个例子:用户访问 /product/details/5,路由系统会把它解析成:
- Controller =
ProductController - Action =
Details - 参数 id =
5
然后框架就会去执行 ProductController.Details(5) 这个方法。就这么简单。
核心要点:路由 = URL 到代码的映射规则。没有路由,你的 API 就没人能找到。
3.2 传统路由(基于约定的路由)
传统路由,也叫基于约定的路由。它是在 Program.cs 或者 Startup.cs 里统一配置的。我个人习惯在项目初期用这种方式,因为一眼就能看到所有路由的模板。
看个例子:
// Program.cs
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"
);
这个模板的意思是:
{controller}— 控制器名,默认是Home{action}— 方法名,默认是Index{id?}— 可选参数,问号表示可省略
所以用户访问 / 时,实际调用的是 HomeController.Index()。访问 /product/list 时,调用 ProductController.List()。
我的经验:传统路由适合结构简单的项目。我曾经在一个大型电商项目里用了纯传统路由,结果路由表越来越长,最后维护起来像在翻字典。后来我果断切到了属性路由。
3.3 属性路由(Attribute Routing)
属性路由,就是在 Controller 和 Action 上直接贴标签(Attribute)。这种方式更灵活,也更直观。我现在做新项目,基本都用属性路由。
看代码:
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
// 处理请求
return Ok(new { Id = id, Name = "Sample Product" });
}
[HttpPost]
public IActionResult CreateProduct([FromBody] Product product)
{
// 创建产品
return CreatedAtAction(nameof(GetProduct), new { id = 1 }, product);
}
}
这里 [Route("api/[controller]")] 中的 [controller] 是个占位符,运行时会被替换成 Products。所以 GetProduct 方法对应的 URL 就是 GET /api/products/5。
属性路由的好处很明显:
- 路由和代码在一起 — 看 Controller 就知道它响应什么 URL
- 支持嵌套和组合 — 可以轻松实现 RESTful 风格
- 支持 HTTP 动词 —
[HttpGet]、[HttpPost]等直接绑定
注意:属性路由和传统路由可以混用,但我建议你选一种。混用容易出问题,比如两个路由匹配到同一个 Action,框架会报错。我曾经在项目里吃过这个亏,排查了半天才发现是路由冲突。
3.4 路由约束
路由约束,就是给参数加规则。比如 id 必须是数字,slug 必须是字母。为什么要加约束?因为用户可能乱输地址,你得保护你的代码。
看几个常用约束:
| 约束 | 示例 | 说明 |
|---|---|---|
:int |
{id:int} |
必须是整数 |
:alpha |
{slug:alpha} |
必须是字母 |
:min(1) |
{id:min(1)} |
最小值 1 |
:regex(^[a-z]+$) |
{slug:regex(^[a-z]+$)} |
正则表达式 |
实际使用:
[HttpGet("products/{id:int:min(1)}")]
public IActionResult GetProduct(int id)
{
// id 一定是大于等于 1 的整数
return Ok();
}
[HttpGet("articles/{slug:alpha}")]
public IActionResult GetArticle(string slug)
{
// slug 一定是纯字母
return Ok();
}
避坑指南:我曾经在项目中遇到一个问题——用户输入 /products/abc,结果返回了 500 错误。原因就是没有加 :int 约束,框架尝试把 "abc" 转成 int 时抛异常了。加上约束后,直接返回 404,优雅多了。
3.5 URL 生成
路由不仅能解析 URL,还能生成 URL。这在很多场景下非常有用,比如:
- 在 View 中生成链接
- 在 API 中返回 Created 响应时生成 Location 头
- 在重定向时生成目标 URL
看个例子:
// 在 Controller 中生成 URL
var url = Url.Action("GetProduct", "Products", new { id = 5 });
// 结果: /Products/GetProduct/5
// 或者用路由名
var url2 = Url.RouteUrl("product_details", new { id = 5 });
// 结果: /product/5
在 Razor View 中:
<a asp-controller="Products" asp-action="Details" asp-route-id="5">
查看产品详情
</a>
这个 Tag Helper 会自动根据路由配置生成正确的 URL。如果你改了路由模板,所有链接会自动更新,不用手动改。
我的建议:永远不要硬编码 URL。用路由生成 URL,这样你的代码更灵活。我见过太多项目因为硬编码 URL,改一次路由就要全局搜索替换,太痛苦了。
3.6 路由优先级与匹配规则
路由匹配是有顺序的。ASP.NET Core 会按照你注册的顺序,从上到下依次匹配。一旦匹配成功,就不再往下找了。
所以要注意:
- 更具体的路由放前面 — 比如
/products/featured要放在/products/{id}前面 - 通配路由放最后 — 比如
{*catchall}这种兜底路由
看个反面教材:
// 错误顺序
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute("api", "api/{controller}/{action}");
// 用户访问 /api/products/list,会先匹配到 default 路由
// 结果:Controller = "api", Action = "products" — 完全不对!
正确的做法是把更具体的路由放前面:
app.MapControllerRoute("api", "api/{controller}/{action}");
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
注意:属性路由的优先级高于传统路由。如果你混用两种路由,属性路由会先被匹配。这个顺序是固定的,你没法改。
3.7 小结
好了,咱们把路由的核心内容过了一遍。总结一下:
- 传统路由适合简单项目,统一配置,一目了然
- 属性路由更灵活,适合 RESTful API 和大型项目
- 路由约束保护你的代码,防止非法输入
- URL 生成避免硬编码,让代码更健壮
- 路由顺序很重要,具体路由放前面
下一章咱们聊聊模型绑定,看看用户提交的数据是怎么变成 C# 对象的。嗯,那个也很有意思。