路由系統 Routing
路由是什麼?
路由就像是郵差送信——用戶發出請求(寄信),路由系統根據 URL(地址)找到對應的 Controller Action(收件人)。
用戶請求 GET /Products/Details/5
↓
路由系統比對 {controller}/{action}/{id?}
↓
找到 ProductsController.Details(5)
↓
回傳結果
傳統路由 Conventional Routing
在 Program.cs 中設定路由模板:
// Program.cs - 設定傳統路由
app.MapControllerRoute(
name: "default", // 路由名稱
pattern: "{controller=Home}/{action=Index}/{id?}" // 路由模板
);
// controller=Home → 預設控制器為 Home
// action=Index → 預設動作為 Index
// id? → id 是可選參數
路由比對範例
| URL | Controller | Action | id |
|---|---|---|---|
/ |
Home | Index | null |
/Products |
Products | Index | null |
/Products/Details |
Products | Details | null |
/Products/Details/5 |
Products | Details | 5 |
屬性路由 Attribute Routing
直接在 Controller 或 Action 上標註路由:
// 使用屬性路由的控制器
[Route("api/[controller]")] // 基底路由,[controller] 會自動替換為類別名稱
public class ProductsController : Controller
{
[Route("")] // 對應 GET /api/Products
[Route("list")] // 也對應 GET /api/Products/list
public IActionResult Index()
{
return View(); // 回傳視圖
}
[Route("{id:int}")] // 對應 GET /api/Products/5,id 必須是整數
public IActionResult Details(int id)
{
return Content($"商品 ID:{id}"); // 回傳文字內容
}
}
路由參數與限制條件
// 路由限制條件範例
public class CatalogController : Controller
{
// {id:int} → id 必須是整數
[HttpGet("catalog/{id:int}")]
public IActionResult ById(int id)
{
return Content($"用 ID 查詢:{id}"); // 整數 ID 查詢
}
// {name:alpha} → name 只能是英文字母
[HttpGet("catalog/{name:alpha}")]
public IActionResult ByName(string name)
{
return Content($"用名稱查詢:{name}"); // 名稱查詢
}
// {slug:regex(^[a-z0-9-]+$)} → 自訂正規表示式
[HttpGet("catalog/slug/{slug:regex(^[[a-z0-9-]]+$)}")]
public IActionResult BySlug(string slug)
{
return Content($"用 Slug 查詢:{slug}"); // Slug 查詢
}
// 可選參數用 ? 表示
[HttpGet("catalog/page/{page:int?}")]
public IActionResult List(int page = 1)
{
return Content($"第 {page} 頁"); // 預設為第 1 頁
}
}
常見路由限制條件
| 限制條件 | 說明 | 範例 |
|---|---|---|
{id:int} |
整數 | 123 |
{name:alpha} |
英文字母 | hello |
{price:decimal} |
十進位數 | 9.99 |
{flag:bool} |
布林值 | true |
{id:min(1)} |
最小值 1 | 1, 100 |
{name:maxlength(20)} |
最大長度 20 | short |
多重路由模式
// Program.cs - 設定多組路由
// 第一組:管理後台路由
app.MapControllerRoute(
name: "admin", // 路由名稱
pattern: "admin/{controller=Dashboard}/{action=Index}/{id?}" // 後台路由
);
// 第二組:預設路由
app.MapControllerRoute(
name: "default", // 路由名稱
pattern: "{controller=Home}/{action=Index}/{id?}" // 預設路由
);
Area 路由
Area(區域)用來將大型專案分組:
// Areas/Admin/Controllers/DashboardController.cs
[Area("Admin")] // 標記屬於 Admin 區域
public class DashboardController : Controller
{
public IActionResult Index()
{
return View(); // 回傳 Areas/Admin/Views/Dashboard/Index.cshtml
}
}
// Program.cs - 設定 Area 路由
app.MapControllerRoute(
name: "areas", // 路由名稱
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}" // Area 路由模板
);
// area:exists → 確認 Area 存在才比對
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:路由模板重複衝突
// ❌ 兩個 Action 路由一模一樣,系統不知道要用哪個
[HttpGet("products/{id}")]
public IActionResult GetById(int id) => Content("By ID");
[HttpGet("products/{name}")]
public IActionResult GetByName(string name) => Content("By Name");
// ✅ 用限制條件區分
[HttpGet("products/{id:int}")] // id 必須是整數
public IActionResult GetById(int id) => Content("By ID");
[HttpGet("products/{name:alpha}")] // name 必須是字母
public IActionResult GetByName(string name) => Content("By Name");
為什麼? 沒有限制條件,
products/5同時符合兩個路由,會造成AmbiguousMatchException。加上:int和:alpha就能明確區分。
❌ 錯誤 2:忘記在 Area Controller 加 [Area] 屬性
// ❌ 少了 [Area] 屬性,路由找不到
public class AdminDashboardController : Controller
{
public IActionResult Index() => View();
}
// ✅ 加上 [Area("Admin")] 屬性
[Area("Admin")] // 標記屬於 Admin 區域
public class AdminDashboardController : Controller
{
public IActionResult Index() => View(); // 正確對應 Area 路由
}
為什麼? Area 路由需要
[Area]屬性來比對{area:exists},沒標記就無法匹配路由。
❌ 錯誤 3:路由順序錯誤
// ❌ 萬用路由放在前面,後面的特定路由永遠不會被匹配
app.MapControllerRoute("catchall", "{*url}", new { controller="Home", action="NotFound" });
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
// ✅ 特定路由放前面,萬用路由放最後
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute("catchall", "{*url}", new { controller="Home", action="NotFound" });
為什麼? 路由是依序比對的,萬用路由
{*url}會匹配所有 URL,放在前面就會把所有請求攔截掉。