Routing đến Controller Actions trong ASP.NET Core
Đây là hướng dẫn toàn diện về routing trong ASP.NET Core MVC, giải thích cách Routing Middleware (phần mềm trung gian định tuyến) khớp URL và ánh xạ chúng đến controller actions.
Tổng quan
ASP.NET Core controllers sử dụng Routing middleware để:
- Khớp URL của các incoming requests (request đến)
- Ánh xạ chúng đến actions
- Tạo URL cho các links trong responses
Các route templates (mẫu định tuyến) có thể được định nghĩa trong Program.cs hoặc dưới dạng attributes (thuộc tính) và mô tả cách URL paths được khớp với actions.
Actions có thể là conventionally-routed (định tuyến theo quy ước) hoặc attribute-routed (định tuyến theo thuộc tính). Đặt route trên controller hoặc action làm cho nó được attribute-routed.
Thiết lập Conventional Routes (Định tuyến theo quy ước)
Template ASP.NET Core MVC tạo ra code conventional routing:
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();Hiểu route template mặc định
Route template "{controller=Home}/{action=Index}/{id?}":
- Khớp URL như
/Products/Details/5 - Trích xuất route values (giá trị định tuyến):
{ controller = Products, action = Details, id = 5 } {controller=Home}định nghĩaHomelà controller mặc định{action=Index}định nghĩaIndexlà action mặc định{id?}làm choidlà tùy chọn (ký tự?)
Ví dụ Controller
public class ProductsController : Controller
{
public IActionResult Details(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}Khớp URL path
Route khớp các URL path sau:
/Products/List→ProductsController.List/Blog/Article/17→BlogController.Article(model bindidthành 17)/Home/Index/17→HomeController.Index/Home/Index→HomeController.Index/Home→HomeController.Index/→HomeController.Index
Phương thức tiện lợi
Phương thức MapDefaultControllerRoute() thay thế cho lời gọi MapControllerRoute() đầy đủ:
app.MapDefaultControllerRoute();
Chi tiết Conventional Routing
Đặc điểm
- Routes chỉ dựa trên tên controller và action
- Không dựa trên namespaces, vị trí file nguồn, hay tham số phương thức
- Giúp đơn giản hóa code và làm UI dễ đoán hơn
- Không cần tạo URL patterns mới cho mỗi action
Nhiều Conventional Routes
Có thể cấu hình nhiều routes:
app.MapControllerRoute(name: "blog",
pattern: "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");Route blog là một dedicated conventional route (định tuyến quy ước chuyên dụng) vì:
- Nó dùng conventional routing
- Chuyên dụng cho một action cụ thể
- Không có
controllervàactionlà tham số - Route có ưu tiên cao hơn để khớp (được thêm trước)
Thứ tự Conventional Routing
- Routes được đánh giá theo thứ tự thêm vào
- Matches từ route trước có ưu tiên cao hơn
- Đặt routes với areas trước (cụ thể hơn routes không có areas)
- Tránh các greedy routes (với catch-all parameters) đầu tiên—chúng có thể ngăn các route khác khớp
Giải quyết Ambiguous Actions (Action mơ hồ)
Khi nhiều endpoints khớp, routing phải chọn ứng viên tốt nhất hoặc throw exception:
public class Products33Controller : Controller
{
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpPost]
public IActionResult Edit(int id, Product product)
{
return ControllerContext.MyDisplayRouteInfo(id, product.name);
}
}Edit(int, Product)được chọn cho HTTPPOSTrequestsEdit(int)được chọn cho các HTTP verbs khác (thường làGET)- Thuộc tính
[HttpPost]giúp routing phân biệt giữa các actions
Tên Conventional Route
Tên route được dùng để tạo URL:
app.MapControllerRoute(name: "blog",
pattern: "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");Tên route:
- Không ảnh hưởng đến URL matching
- Chỉ được dùng để tạo URL
- Phải duy nhất trong toàn ứng dụng
Attribute Routing cho REST APIs
REST APIs nên dùng attribute routing để mô hình hóa chức năng như tài nguyên với HTTP verbs (động từ HTTP) đại diện cho các operations (thao tác).
Thiết lập cơ bản
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); var app = builder.Build(); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
Ví dụ HomeController với Attribute Routes
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 Index khớp: /, /Home, /Home/Index, hoặc /Home/Index/3
Điểm khác biệt chính so với Conventional Routing
- Yêu cầu input tường minh hơn
- Cung cấp kiểm soát chính xác về route templates nào áp dụng cho mỗi action
- Tên controller và action không đóng vai trò trong matching (trừ khi dùng token replacement)
Token Replacement (Thay thế token)
Các token [action], [area], và [controller] được thay thế bằng giá trị thực tế:
public class HomeController : Controller
{
[Route("")]
[Route("Home")]
[Route("[controller]/[action]")]
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}
[Route("[controller]/[action]")]
public IActionResult About()
{
return ControllerContext.MyDisplayRouteInfo();
}
}Áp dụng Routes cho Controllers
[Route("[controller]/[action]")]
public class HomeController : Controller
{
[Route("~/")]
[Route("/Home")]
[Route("~/Home/Index")]
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}
public IActionResult About()
{
return ControllerContext.MyDisplayRouteInfo();
}
}Routes bắt đầu bằng / hoặc ~/ không kết hợp với controller routes.
Tên Routing được đặt trước (Reserved Routing Names)
Cho Controllers hoặc Razor Pages:
actionareacontrollerhandlerpage
Cho Razor views hoặc Razor Pages:
pageusingnamespaceinjectsectioninheritsmodeladdTagHelperremoveTagHelper
Không dùng các tên này cho link generation, model bound parameters, hay top-level properties.
HTTP Verb Templates (Mẫu động từ HTTP)
ASP.NET Core cung cấp:
[HttpGet][HttpPost][HttpPut][HttpDelete][HttpHead][HttpPatch][Route]
Tất cả đều là route templates.
Ví dụ với HTTP Verb Attributes
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
[HttpGet] // GET /api/test2
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}
[HttpGet("{id}")] // GET /api/test2/xyz
public IActionResult GetProduct(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("int/{id:int}")] // GET /api/test2/int/3
public IActionResult GetIntProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("int2/{id}")] // GET /api/test2/int2/3
public IActionResult GetInt2Product(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}Tên Route
Định nghĩa tên route để tạo URL:
[ApiController]
public class Products2ApiController : ControllerBase
{
[HttpGet("/products2/{id}", Name = "Products_List")]
public IActionResult GetProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}Tên route:
- Không ảnh hưởng đến URL matching
- Chỉ dùng để tạo URL
- Phải duy nhất trong toàn ứng dụng
Kết hợp Attribute Routes
Kết hợp route attributes trên controller và action để giảm lặp lại:
[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 path
/products→ProductsApi.ListProducts - URL path
/products/5→ProductsApi.GetProduct(int)
Nhiều Routes trên Controllers
[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
[HttpPost("Buy")] // Khớp 'Products6/Buy' và 'Store/Buy'
[HttpPost("Checkout")] // Khớp 'Products6/Checkout' và 'Store/Checkout'
public IActionResult Buy()
{
return ControllerContext.MyDisplayRouteInfo();
}
}Thứ tự Attribute Route
Routes được xử lý theo độ cụ thể:
// Routes cụ thể hơn có ưu tiên cao hơn
[Route("blog/search/{topic}")] // Ưu tiên cao hơn
[Route("blog/{*article}")] // Ưu tiên thấp hơnDùng thuộc tính Order:
[Route("")]
[Route("Home", Order = 2)]
[Route("Home/MyIndex")]
public IActionResult MyIndex()
{
return ControllerContext.MyDisplayRouteInfo();
}Cảnh báo: Hãy tránh phụ thuộc vào Order nếu có thể. Dùng tên route để override thay thế.
Tùy chỉnh Token Replacement
Implement IOutboundParameterTransformer để tùy chỉnh token replacement:
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();
}
}Đăng ký bằng RouteTokenTransformerConvention:
using Microsoft.AspNetCore.Mvc.ApplicationModels;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});
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();Nhiều Attribute Routes trên Actions
Định nghĩa nhiều routes dẫn đến cùng một action:
[Route("[controller]")]
public class Products13Controller : Controller
{
[Route("")] // Khớp 'Products13'
[Route("Index")] // Khớp 'Products13/Index'
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}
}Tham số tùy chọn, giá trị mặc định và ràng buộc
public class Products14Controller : Controller
{
[HttpPost("product14/{id:int}")]
public IActionResult ShowProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}Ràng buộc :int giới hạn segment chỉ nhận số nguyên.
Custom Route Attributes (Thuộc tính route tùy chỉnh)
Implement IRouteTemplateProvider:
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
{
// GET /api/MyTestApi
[HttpGet]
public IActionResult Get()
{
return ControllerContext.MyDisplayRouteInfo();
}
}Tùy chỉnh Application Model (Mô hình ứng dụng)
Tùy chỉnh routing bằng application model conventions:
public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
private readonly string _baseNamespace;
public NamespaceRoutingConvention(string baseNamespace)
{
_baseNamespace = baseNamespace;
}
public void Apply(ControllerModel controller)
{
var hasRouteAttributes = controller.Selectors.Any(selector =>
selector.AttributeRouteModel != null);
if (hasRouteAttributes)
{
return;
}
var namespc = controller.ControllerType.Namespace;
if (namespc == null)
return;
var template = new StringBuilder();
template.Append(namespc, _baseNamespace.Length + 1,
namespc.Length - _baseNamespace.Length - 1);
template.Replace('.', '/');
template.Append("/[controller]/[action]/{id?}");
foreach (var selector in controller.Selectors)
{
selector.AttributeRouteModel = new AttributeRouteModel()
{
Template = template.ToString()
};
}
}
}Đăng ký trong Program.cs:
using My.Application.Controllers;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(
new NamespaceRoutingConvention(typeof(HomeController).Namespace!));
});
var app = builder.Build();Mixed Routing (Định tuyến hỗn hợp)
ASP.NET Core hỗ trợ kết hợp conventional và attribute routing:
// Conventional routing cho HTML pages
app.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// Attribute routing cho REST APIs
app.MapControllers();Quan trọng: Bất kỳ route attribute nào trên controller sẽ làm tất cả actions trong controller đó được attribute-routed.
Routing với ký tự đặc biệt
Routing với ký tự đặc biệt có thể dẫn đến kết quả không mong muốn:
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null || todoItem.Name == null)
{
return NotFound();
}
return todoItem.Name;
}| ASCII | Encoded |
|---|---|
/ | %2F |
| (khoảng trắng) | + |
Route parameters không phải lúc nào cũng được URL decode—hãy cẩn thận với ký tự đặc biệt.
Tạo URL và Ambient Values (Giá trị môi trường)
Dùng IUrlHelper
public class UrlGenerationController : Controller
{
public IActionResult Source()
{
// Tạo ra /UrlGeneration/Destination
var url = Url.Action("Destination");
return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
}
public IActionResult Destination()
{
return ControllerContext.MyDisplayRouteInfo();
}
}Routing tạo URL bằng cách kết hợp:
- Ambient values (giá trị môi trường) từ request hiện tại:
{ controller = "UrlGeneration", action = "Source" } - Giá trị truyền vào
Url.Action:{ controller = "UrlGeneration", action = "Destination" } - Route template:
{controller}/{action}/{id?} - Kết quả:
/UrlGeneration/Destination
Ví dụ Attribute Routing
public class UrlGenerationAttrController : Controller
{
[HttpGet("custom")]
public IActionResult Source()
{
var url = Url.Action("Destination");
return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
}
[HttpGet("custom/url/to/destination")]
public IActionResult Destination()
{
return ControllerContext.MyDisplayRouteInfo();
}
}Action Source tạo ra custom/url/to/destination.
Tạo URL theo tên Action
public IActionResult Index()
{
var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
return Content(url!); // /Products/Buy/17?color=red
}Tạo URL tuyệt đối:
public IActionResult Index2()
{
var url = Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme);
// Trả về https://localhost:5001/Products/Buy/17
return Content(url!);
}Tạo URL theo Route
public class UrlGeneration2Controller : Controller
{
[HttpGet("")]
public IActionResult Source()
{
var url = Url.RouteUrl("Destination_Route");
return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
}
[HttpGet("custom/url/to/destination2", Name = "Destination_Route")]
public IActionResult Destination()
{
return ControllerContext.MyDisplayRouteInfo();
}
}Trong Razor:
<h1>Test Links</h1>
<ul>
<li><a href="@Url.RouteUrl("Destination_Route")">Test Destination_Route</a></li>
</ul>Tạo URL trong HTML và Razor
IHtmlHelpercung cấpHtml.BeginFormvàHtml.ActionLinkHtml.BeginRouteFormvàHtml.RouteLinkcho các URL dựa trên route- TagHelpers (
formvà<a>) dùngIUrlHelper
Tạo URL trong Action Results
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int id, Customer customer)
{
if (ModelState.IsValid)
{
// Cập nhật DB với thông tin mới.
ViewData["Message"] = $"Successful edit of customer {id}";
return RedirectToAction("Index");
}
return View(customer);
}Các factory methods như RedirectToAction và CreatedAtAction sử dụng URL generation.
Areas (Khu vực)
Areas tổ chức chức năng liên quan vào các nhóm:
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.MapAreaControllerRoute("blog_route", "Blog",
"Manage/{controller}/{action}/{id?}");
app.MapControllerRoute("default_route", "{controller}/{action}/{id?}");
app.Run();URL path /Manage/Users/AddUser tạo route values: { area = Blog, controller = Users, action = AddUser }
Ví dụ Area Controller
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace1
{
[Area("Blog")]
public class UsersController : Controller
{
// GET /manage/users/adduser
public IActionResult AddUser()
{
var area = ControllerContext.ActionDescriptor.RouteValues["area"];
var actionName = ControllerContext.ActionDescriptor.ActionName;
var controllerName = ControllerContext.ActionDescriptor.ControllerName;
return Content($"area name:{area}" +
$" controller:{controllerName} action name: {actionName}");
}
}
}Thuộc tính [Area("Blog")] chỉ định controller thuộc một area.
Controller không thuộc Area
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace3
{
// Khớp { area = string.Empty, controller = Users, action = AddUser }
// Khớp { area = null, controller = Users, action = AddUser }
// Khớp { controller = Users, action = AddUser }
public class UsersController : Controller
{
// GET /users/adduser
public IActionResult AddUser()
{
var area = ControllerContext.ActionDescriptor.RouteValues["area"];
var actionName = ControllerContext.ActionDescriptor.ActionName;
var controllerName = ControllerContext.ActionDescriptor.ControllerName;
return Content($"area name:{area}" +
$" controller:{controllerName} action name: {actionName}");
}
}
}Sticky Areas (Area dính)
Routes trong area dùng area như ambient value để tạo URL:
app.MapAreaControllerRoute("duck_route",
areaName: "Duck",
pattern: "Manage/{controller}/{action}/{id?}");
app.MapControllerRoute(name: "default",
pattern: "Manage/{controller=Home}/{action=Index}/{id?}");using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace4
{
[Area("Duck")]
public class UsersController : Controller
{
// GET /Manage/users/GenerateURLInArea
public IActionResult GenerateURLInArea()
{
// Dùng giá trị 'ambient' của area.
var url = Url.Action("Index", "Home");
// Trả về /Manage/Home/Index
return Content(url);
}
// GET /Manage/users/GenerateURLOutsideOfArea
public IActionResult GenerateURLOutsideOfArea()
{
// Dùng giá trị rỗng cho area.
var url = Url.Action("Index", "Home", new { area = "" });
// Trả về /Manage
return Content(url);
}
}
}Chẩn đoán Debug
Để xem đầu ra chẩn đoán routing chi tiết, thiết lập logging:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}Trong appsettings.Development.json.