Nguon: Microsoft Learn · .NET 8.0

Routing đến Controller Actions trong ASP.NET Core

Nguồn: Routing to controller actions in 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 để:

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:

csharp
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?}":

Ví dụ Controller

csharp
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:

Phương thức tiện lợi

Phương thức MapDefaultControllerRoute() thay thế cho lời gọi MapControllerRoute() đầy đủ:

csharp
app.MapDefaultControllerRoute();

Chi tiết Conventional Routing

Đặc điểm

Nhiều Conventional Routes

Có thể cấu hình nhiều routes:

csharp
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ì:

Thứ tự Conventional Routing

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:

csharp
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);
    }
}

Tên Conventional Route

Tên route được dùng để tạo URL:

csharp
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:

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

csharp
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

csharp
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

Token Replacement (Thay thế token)

Các token [action], [area], và [controller] được thay thế bằng giá trị thực tế:

csharp
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

csharp
[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:

Cho Razor views hoặc Razor Pages:

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:

Tất cả đều là route templates.

Ví dụ với HTTP Verb Attributes

csharp
[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:

csharp
[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

Tên route:

Kết hợp Attribute Routes

Kết hợp route attributes trên controller và action để giảm lặp lại:

csharp
[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);
    }
}

Nhiều Routes trên Controllers

csharp
[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ể:

csharp
// 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ơn

Dùng thuộc tính Order:

csharp
[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:

csharp
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:

csharp
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:

csharp
[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

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
// 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:

csharp
[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;
}
ASCIIEncoded
/%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

csharp
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:

Ví dụ Attribute Routing

csharp
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

csharp
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:

csharp
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

csharp
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:

cshtml
<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

Tạo URL trong Action Results

csharp
[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ư RedirectToActionCreatedAtAction 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:

csharp
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

csharp
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

csharp
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:

csharp
app.MapAreaControllerRoute("duck_route",
                                     areaName: "Duck",
                                     pattern: "Manage/{controller}/{action}/{id?}");
app.MapControllerRoute(name: "default",
                             pattern: "Manage/{controller=Home}/{action=Index}/{id?}");
csharp
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:

json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Trong appsettings.Development.json.