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# 对象的。嗯,那个也很有意思。