Routing là gì?

ASP.NET Core controllers sử dụng Routing middleware để match URLs của incoming requests và map chúng đến các actions. Route templates:
  • Được định nghĩa tại startup trong Program.cs hoặc trong attributes
  • Mô tả cách URL paths được match đến actions
  • Được dùng để generate URLs cho links trong responses
Actions có thể là conventionally-routed hoặc attribute-routed. Đặt route trên controller hoặc action sẽ biến nó thành attribute-routed.

Thiết lập Conventional Route

Cấu hình mặc định

ASP.NET Core MVC template tạo conventional routing code như sau:
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

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

app.Run();

Giải thích Route Template

{controller=Home}/{action=Index}/{id?}
│            │           │
│            │           └── id là optional (?)
│            └── action mặc định: Index
└── controller mặc định: Home
URL PathControllerActionid
/Products/Details/5ProductsDetails5
/Products/DetailsProductsDetailsnull (model binding set 0)
/HomeHomeIndexnull
/HomeIndexnull
Lưu ý: Route template định nghĩa id là optional. Khi id bị omit khỏi URL, model binding set id thành 0, và thường không tìm thấy entity nào trong database matching id == 0.

Conventional Routing

Nguyên tắc cơ bản

Conventional routing thiết lập quy ước cho URL paths:
SegmentMaps to
{controller=Home}Tên controller (suffix “Controller” được loại bỏ)
{action=Index}Tên action method
{id?}Optional id — maps to model entity

Ví dụ Controller

public class ProductsController : Controller
{
    public IActionResult Details(int id)
    {
        // id = 5 khi URL là /Products/Details/5
        return View();
    }
}

Lợi ích của Conventional Routing

  • URL nhất quán cho CRUD operations
  • Không cần tạo URL pattern mới cho mỗi action
  • UI dễ dự đoán
Khuyến nghị: Hầu hết apps nên chọn một routing scheme cơ bản và descriptive để URLs dễ đọc và có ý nghĩa.

Nhiều Conventional Routes

Có thể cấu hình nhiều conventional routes bằng cách thêm nhiều MapControllerRouteMapAreaControllerRoute:
app.MapControllerRoute(
    name: "blog",
    pattern: "blog/{*article}",
    defaults: new { controller = "Blog", action = "Article" });

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
RoutePatternKhi nào match
blogblog/{*article}/blog, /blog/abc, /blog/abc/def
default{controller=Home}/{action=Index}/{id?}Mọi URL còn lại

Thứ tự ưu tiên

  • Routes thêm trước có ưu tiên cao hơn
  • Routes cụ thể nên đặt trước
  • Routes greedy (catch-all {*article}) nên đặt sau

Attribute Routing

Giới thiệu

REST APIs nên dùng attribute routing để model app’s functionality như một tập resources, với operations được biểu diễn bằng HTTP verbs.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();  // Map attribute-routed controllers

app.Run();

Cấu hình Controller với Attribute Routing

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    [HttpGet]                        // GET /api/products
    public IActionResult ListProducts() => Ok(_products);

    [HttpGet("{id}")]                // GET /api/products/5
    public IActionResult GetProduct(int id)
    {
        var product = _products.FirstOrDefault(p => p.Id == id);
        return product is null ? NotFound() : Ok(product);
    }

    [HttpPost]                       // POST /api/products
    public IActionResult CreateProduct([FromBody] Product product)
    {
        _products.Add(product);
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }

    [HttpPut("{id}")]                // PUT /api/products/5
    public IActionResult UpdateProduct(int id, [FromBody] Product product) { /* ... */ }

    [HttpDelete("{id}")]             // DELETE /api/products/5
    public IActionResult DeleteProduct(int id) { /* ... */ }
}

HTTP Verb Templates

ASP.NET Core cung cấp các HTTP verb templates:
AttributeHTTP MethodVí dụ URL
[HttpGet]GETGET /api/products
[HttpPost]POSTPOST /api/products
[HttpPut]PUTPUT /api/products/5
[HttpDelete]DELETEDELETE /api/products/5
[HttpPatch]PATCHPATCH /api/products/5
[HttpHead]HEADHEAD /api/products

Kết hợp Routes

[Route("products")]
public class ProductsApiController : ControllerBase
{
    [HttpGet]           // GET /products
    public IActionResult ListProducts() => Ok(_products);

    [HttpGet("{id}")]   // GET /products/5
    public IActionResult GetProduct(int id) { /* ... */ }
}
Route trên ControllerRoute trên ActionURL cuối cùng
[Route("products")][HttpGet]/products
[Route("products")][HttpGet("{id}")]/products/{id}

Token Replacement

Dùng [controller][action] để tự động thay thế tên:
[Route("[controller]/[action]")]
public class HomeController : Controller
{
    [Route("~/")]        // Override, không kết hợp với controller route
    [Route("/Home")]
    public IActionResult Index() => View();

    public IActionResult About() => View();
    // → /Home/About
}

Route với Route Constraints

[HttpGet("{id}")]            // GET /products/abc → Match
public IActionResult GetProduct(string id) { /* id = "abc" */ }

[HttpGet("int/{id:int}")]     // GET /products/int/abc → 404 Not Found
                               // GET /products/int/5   → Match
public IActionResult GetIntProduct(int id) { /* id = 5 */ }
ConstraintMô tảVí dụ
:intChỉ integer{id:int}
:longChỉ long{id:long}
:floatChỉ float{id:float}
:boolChỉ boolean{id:bool}
:alphaChỉ alphabet{name:alpha}
:length(min,max)Giới hạn độ dài{name:length(3,20)}
:range(min,max)Giới hạn giá trị{id:range(1,100)}

Giải quyết Ambiguous Actions

Khi hai endpoints match qua routing:
public class ProductsController : Controller
{
    public IActionResult Edit(int id)
    {
        // Hiển thị form edit — GET /Products/Edit/5
        return View(GetProduct(id));
    }

    [HttpPost]
    public IActionResult Edit(int id, Product product)
    {
        // Xử lý form submit — POST /Products/Edit/5
        if (!ModelState.IsValid) return View(product);
        UpdateProduct(product);
        return RedirectToAction(nameof(Index));
    }
}
HTTP MethodAction được chọnLý do
GETEdit(int id)Không có [HttpPost]
POSTEdit(int id, Product)[HttpPost] match
Lưu ý: Nếu routing không thể chọn candidate tốt nhất, nó sẽ throw AmbiguousMatchException.

Route Names

Route names cung cấp tên logic cho route, dùng cho URL generation:
[HttpGet("/products/{id}", Name = "Products_Details")]
public IActionResult GetProduct(int id) { /* ... */ }
// URL Generation
var url = Url.Link("Products_Details", new { id = 5 });
// → "/products/5"
Route NameDùng cho
URL Matching❌ Không ảnh hưởng
URL Generation✅ Dùng để generate links

Reserved Route Parameter Names

Khi dùng Controllers hoặc Razor Pages

Từ khóaGhi chú
actionReserved
areaReserved
controllerReserved
handlerReserved
pageReserved
// ❌ Lỗi phổ biến — dùng "page" làm route parameter
[Route("/articles/{page}")]
public IActionResult ListArticles(int page) { /* ... */ }

Trong Razor View/Page Context

page, using, namespace, inject, section, inherits, model, addTagHelper, removeTagHelper là reserved.

So sánh Conventional vs Attribute Routing

Tiêu chíConventional RoutingAttribute Routing
Cấu hìnhProgram.cs[Route] trên controller/action
URL dựa trênController/Action namesTùy chỉnh hoàn toàn
REST APIs❌ Ít phù hợp✅ Phù hợp nhất
Web UI apps✅ Phù hợpCó thể dùng
Fine-grained control❌ Hạn chế✅ Kiểm soát chính xác
DRY✅ DRY⚠️ Có thể lặp lại
Khuyến nghị: REST APIs → Attribute Routing. Web UI apps với CRUD → Conventional Routing là điểm khởi đầu tốt.

Tài liệu tham khảo