ASP.NET Core: Định tuyến tới các action của controller trong ASP.NET Core


Khóa học qua video:
Lập trình Python All C# Lập trình C Java SQL Server PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên

Các controller của ASP.NET Core sử dụng Định tuyến middleware để khớp URL của các yêu cầu đến và ánh xạ chúng tới các action. Các mẫu định tuyến:

  • Được định nghĩa khi khởi động trong Program.cs hoặc trong các attribute.
  • Mô tả cách đường dẫn URL khớp với các action.
  • Được sử dụng để tạo URL cho các liên kết. Các liên kết được tạo thường được trả về trong phản hồi.

Các action được định tuyến theo quy ước hoặc được định tuyến theo attribute. Việc đặt một định tuyến trên controller hoặc hành động sẽ làm cho nó được định tuyến theo attribute. Xem Định tuyến hỗn hợp để biết thêm thông tin.

Tài liệu này:

  • Giải thích sự tương tác giữa MVC và định tuyến:
    • Cách các ứng dụng MVC điển hình sử dụng các tính năng định tuyến.
    • Bao gồm cả hai:
    • Xem Định tuyến để biết chi tiết định tuyến nâng cao.
  • Đề cập đến hệ thống định tuyến mặc định được gọi là định tuyến điểm cuối. Có thể sử dụng controller với phiên bản định tuyến trước đó cho mục đích tương thích. Xem hướng dẫn di chuyển 2.2-3.0  để biết hướng dẫn.

Thiết lập định tuyến thông thường

Mẫu ASP.NET Core MVC tạo mã định tuyến thông thường tương tự 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();

MapControllerRoute được sử dụng để tạo một định tuyến duy nhất. Định tuyến duy nhất được đặt tên là định tuyến default. Hầu hết các ứng dụng có controller và view đều sử dụng mẫu định tuyến tương tự như định tuyến default. API REST nên sử dụng định tuyến thuộc tính.

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

Mẫu định tuyến "{controller=Home}/{action=Index}/{id?}":

  • Khớp với đường dẫn URL như /Products/Details/5
  • Trích xuất các giá trị định tuyến { controller = Products, action = Details, id = 5 } bằng cách mã hóa đường dẫn. Việc trích xuất các giá trị định tuyến dẫn đến kết quả khớp nếu ứng dụng có controller được đặt tên là ProductsController và một action Details:
public class ProductsController : Controller
{
    public IActionResult Details(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
  • MyDisplayRouteInfo được cung cấp bởi gói NuGet Rick.Docs.Samples.RouteInfo và hiển thị thông tin định tuyến.
  • Model /Products/Details/5 liên kết giá trị của id = 5 để đặt tham số id thành 5. Xem Liên kết mô hình để biết thêm chi tiết.
  • {controller=Home} định nghĩa Home là controller mặc định.
  • {action=Index} định nghĩa Index là action mặc định.
  • Ký tự ? trong {id?} định nghĩa id là tùy chọn.
    • Các tham số định tuyến mặc định và tùy chọn không cần phải có trong đường dẫn URL để khớp. Xem Tham khảo mẫu định tuyến để biết mô tả chi tiết về cú pháp mẫu định tuyến.
  • Phù hợp với đường dẫn URL /.
  • Tạo ra các giá trị định tuyến { controller = Home, action = Index }.

Các giá trị cho controller và action sử dụng các giá trị mặc định. id không tạo ra giá trị vì không có phân đoạn tương ứng trong đường dẫn URL. / chỉ khớp nếu tồn tại HomeController và action Index:

public class HomeController : Controller
{
    public IActionResult Index() { ... }
}

Sử dụng định nghĩa controller và mẫu định tuyến trước đó, action HomeController.Index được chạy cho các đường dẫn URL sau:

  • /Home/Index/17
  • /Home/Index
  • /Home
  • /

Đường dẫn URL / sử dụng controller Home mặc định mẫu định tuyến và action Index. Đường dẫn URL /Home sử dụng action Index mặc định của mẫu định tuyến.

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

app.MapDefaultControllerRoute();

Thay thế:

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

Quan trọng

Định tuyến được định cấu hình bằng middleware UseRouting và UseEndpoints. Để sử dụng controller:

Các ứng dụng thường không cần gọi UseRouting hoặc UseEndpointsWebApplicationBuilder định cấu hình đường dẫn middleware bao bọc middleware được thêm vào Program.cs bằng UseRouting và UseEndpoints. Để biết thêm thông tin, hãy xem Định tuyến trong ASP.NET Core.

Định tuyến thông thường

Định tuyến thông thường được sử dụng với controller và view. Định tuyến default:

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

Trên đây là một ví dụ về một định tuyến thông thường. Nó được gọi là định tuyến thông thường vì nó thiết lập quy ước cho đường dẫn URL:

  • Đoạn đường dẫn đầu tiên, {controller=Home}, ánh xạ tới tên controller.
  • Phân đoạn thứ hai, {action=Index}, ánh xạ tới tên action.
  • Phân đoạn thứ ba, {id?} được sử dụng cho một tùy chọn id? trong {id?} làm cho nó thày tùy chọn. id được sử dụng để ánh xạ tới một thực thể model.

Sử dụng định tuyến default, đường dẫn URL:

  • /Products/List ánh xạ tới action ProductsController.List.
  • /Blog/Article/17 ánh xạ tới BlogController.Article và model thường liên kết tham số id với 17.

Ánh xạ này:

  • Chỉ dựa trên controller và tên action.
  • Không dựa trên namespace, vị trí tệp nguồn hoặc tham số phương thức.

Việc sử dụng định tuyến thông thường với định tuyến mặc định cho phép tạo ứng dụng mà không cần phải đưa ra mẫu URL mới cho mỗi hành động. Đối với ứng dụng có action kiểu CRUD, tính nhất quán của các URL trên các controller:

  • Giúp đơn giản hóa mã.
  • Làm cho giao diện người dùng dễ dự đoán hơn.

Cảnh báo

id trong đoạn code trên được định nghĩa là tùy chọn theo mẫu định tuyến. Các action có thể thực thi mà không cần ID tùy chọn được cung cấp như một phần của URL. Nói chung, khi id bị bỏ qua khỏi URL thì:

  • id được đặt thành 0 bằng ràng buộc model.
  • Không tìm thấy thực thể nào trong cơ sở dữ liệu phù hợp id == 0.

Định tuyến thuộc tính cung cấp khả năng kiểm soát chi tiết để tạo ID cần thiết cho một số action chứ không phải cho những cái khác. Theo quy ước, tài liệu bao gồm các tham số tùy chọn như id khi chúng có thể xuất hiện khi sử dụng đúng.

Hầu hết các ứng dụng nên chọn sơ đồ định tuyến cơ bản và mang tính mô tả để URL dễ đọc và có ý nghĩa. Định tuyến thông thường mặc định {controller=Home}/{action=Index}/{id?}:

  • Hỗ trợ sơ đồ định tuyến cơ bản và mô tả.
  • Là điểm khởi đầu hữu ích cho các ứng dụng dựa trên giao diện người dùng.
  • Là mẫu định tuyến duy nhất cần thiết cho nhiều ứng dụng giao diện người dùng web. Đối với các ứng dụng giao diện người dùng web lớn hơn, một lộ trình khác sử dụng Area thường là tất cả những gì cần thiết.

MapControllerRoute và MapAreaRoute:

  • Tự động gán giá trị thứ tự cho điểm cuối của chúng dựa trên thứ tự chúng được gọi.

Định tuyến điểm cuối trong ASP.NET Core:

  • Không có khái niệm về định tuyến.
  • Không cung cấp đảm bảo thứ tự cho việc thực hiện khả năng mở rộng, tất cả các điểm cuối đều được xử lý cùng một lúc.

Bật Ghi nhật ký để xem cách triển khai định tuyến tích hợp, chẳng hạn như Định tuyến, khớp các yêu cầu.

Định tuyến thuộc tính sẽ được giải thích sau trong tài liệu này.

Nhiều định tuyến thông thường

Nhiều định tuyến thông thường có thể được định cấu hình bằng cách thêm nhiều lệnh gọi hơn vào MapControllerRoute và MapAreaControllerRoute. Làm như vậy cho phép xác định nhiều quy ước hoặc thêm các định tuyến thông thường dành riêng cho một action cụ thể, chẳng hạn như:

app.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");

Định tuyến blog trong đoạn code trên là định tuyến thông thường chuyên dụng. Nó được gọi là định tuyến thông thường chuyên dụng vì:

Bởi vì controller và action không xuất hiện trong mẫu định tuyến "blog/{*article}" dưới dạng tham số nên:

  • Chúng chỉ có thể có các giá trị mặc định { controller = "Blog", action = "Article" }.
  • Định tuyến này luôn ánh xạ tới action BlogController.Article.

/Blog/Blog/Article và /Blog/{any-string} là các đường dẫn URL duy nhất phù hợp với định tuyến blog.

Trong ví dụ trên:

  • Định tuyến blog có mức độ ưu tiên cao hơn cho các so khớp so với định tuyến default vì nó được thêm đầu tiên.
  • Là một ví dụ về định tuyến kiểu Slug trong đó thông thường có tên bài viết như một phần của URL.

Cảnh báo

Trong ASP.NET Core, việc định tuyến không:

  • Định nghĩa một khái niệm gọi là định tuyến (route)UseRouting thêm kết hợp định tuyến vào đường dẫn middleware. Middleware UseRouting xem xét tập hợp các điểm cuối được xác định trong ứng dụng và chọn điểm cuối phù hợp nhất dựa trên yêu cầu.
  • Cung cấp các đảm bảo về thứ tự thực hiện khả năng mở rộng như IRouteConstraint hoặc IActionConstraint.

Xem Định tuyến để biết tài liệu tham khảo về định tuyến.

Thứ tự định tuyến thông thường

Định tuyến thông thường chỉ khớp với sự kết hợp giữa action và controller được ứng dụng xác định. Điều này nhằm đơn giản hóa các trường hợp các định tuyến thông thường chồng chéo lên nhau. Việc thêm các định tuyến bằng MapControllerRouteMapDefaultControllerRoute và MapAreaControllerRoute sẽ tự động gán giá trị thứ tự cho các điểm cuối của chúng dựa trên thứ tự chúng được gọi. Các so khớp từ định tuyến xuất hiện trước có mức độ ưu tiên cao hơn. Định tuyến thông thường phụ thuộc vào thứ tự. Nói chung, các định tuyến có khu vực nên được đặt sớm hơn vì chúng cụ thể hơn các định tuyến không có khu vực. Các định tuyến thông thường chuyên dụng với các tham số định tuyến tổng hợp như {*article} có thể khiến định tuyến trở nên quá tham lam, nghĩa là nó khớp với các URL mà bạn dự định khớp với các định tuyến khác. Đặt các định tuyến tham lam sau này vào bảng lộ trình để ngăn chặn các định tuyến tham lam.

Giải quyết các action mơ hồ

Khi hai điểm cuối khớp nhau thông qua định tuyến, việc định tuyến phải thực hiện một trong các thao tác sau:

  • Chọn ứng viên tốt nhất.
  • Ném một ngoại lệ.

Ví dụ:

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

Controller ở trên định nghĩa hai action khớp với nhau:

  • Đường dẫn URL /Products33/Edit/17
  • Dữ liệu định tuyến { controller = Products33, action = Edit, id = 17 }.

Đây là mẫu điển hình cho controller MVC:

  • Edit(int) hiển thị một form để chỉnh sửa một sản phẩm.
  • Edit(int, Product) xử lý form đã đăng.

Để giải quyết định tuyến chính xác:

  • Edit(int, Product) được chọn khi yêu cầu là HTTP POST.
  • Edit(int) được chọn khi HTTP Verb là bất kỳ thứ gì khác. Edit(int) thường được gọi thông qua GET.

HttpPostAttribution[HttpPost]được cung cấp để định tuyến để nó có thể chọn dựa trên phương thức HTTP của yêu cầu. HttpPostAttribute làm cho Edit(int, Product) khớp tốt hơn Edit(int).

Điều quan trọng là phải hiểu vai trò của các attribute như HttpPostAttribute. Các attribute tương tự được xác định cho các động từ HTTP khác. Trong định tuyến thông thường, thông thường các action sẽ sử dụng cùng một tên action khi chúng là một phần của quy trình làm việc của biểu mẫu hiển thị, gửi biểu mẫu. Ví dụ: xem Kiểm tra hai phương thức hành động Chỉnh sửa.

Nếu việc định tuyến không thể chọn được ứng cử viên tốt nhất, thì ngoại lệ AmbiguousMatchException sẽ được đưa ra, liệt kê nhiều điểm cuối phù hợp.

Tên định tuyến thông thường

Các chuỗi "blog" và "default" trong các ví dụ sau đây là tên định tuyến thông thường:

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 định tuyến sẽ đặt cho định tuyến một tên logic. Định tuyến được đặt tên có thể được sử dụng để tạo URL. Việc sử dụng định tuyến được đặt tên sẽ đơn giản hóa việc tạo URL khi việc sắp xếp các định tuyến có thể khiến việc tạo URL trở nên phức tạp. Tên định tuyến phải có tính ứng dụng duy nhất trên phạm vi rộng.

Tên định tuyến:

  • Không có tác động đến việc khớp URL hoặc xử lý yêu cầu.
  • Chỉ được sử dụng để tạo URL.

Khái niệm tên định tuyến được thể hiện trong định tuyến dưới dạng IEndpointNameMetadata. Các thuật ngữ tên định tuyến và tên điểm cuối:

  • Có thể hoán đổi cho nhau.
  • Cái nào được sử dụng trong tài liệu và mã tùy thuộc vào API được mô tả.

Định tuyến thuộc tính cho API REST

API REST nên sử dụng định tuyến attribute để model hóa chức năng của ứng dụng dưới dạng một tập hợp tài nguyên trong đó các hoạt động được biểu thị bằng HTTP Verb.

Định tuyến attribute sử dụng một tập hợp các attribute để ánh xạ các hành động trực tiếp vào các mẫu định tuyến. Đoạn code sau đây là điển hình cho API REST và được sử dụng trong mẫu tiếp theo:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Trong đoạn code trên, MapControllers được gọi để ánh xạ các controller định tuyến attribute.

Trong ví dụ sau:

  • HomeController khớp với một tập hợp các URL tương tự với những gì định tuyến thông thường mặc định {controller=Home}/{action=Index}/{id?} khớp.
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 HomeController.Index được thực hiện đối với bất kỳ đường dẫn URL nào sau đây: //Home/Home/Index hoặc /Home/Index/3.

Ví dụ này nêu bật sự khác biệt chính về lập trình giữa định tuyến attribute và định tuyến thông thường. Định tuyến attribute yêu cầu nhiều đầu vào hơn để chỉ định định tuyến. Định tuyến mặc định thông thường xử lý các định tuyến ngắn gọn hơn. Tuy nhiên, định tuyến attribute cho phép và yêu cầu kiểm soát chính xác mẫu định tuyến nào áp dụng cho từng action.

Với định tuyến attribute, tên controller và action không đóng vai trò nào trong action nào được khớp, trừ khi  sử dụng thay thế mã thông báo. Ví dụ sau khớp với các URL giống như ví dụ trên:

public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

Đoạn code sau sử dụng thay thế mã thông báo cho action và controller:

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

Đoạn code sau áp dụng [Route("[controller]/[action]")] cho controller:

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

Trong đoạn code trên, các mẫu phương thức Index phải thêm vào trước / hoặc  ~/ vào các mẫu định tuyến. Các mẫu định tuyến được áp dụng cho một action bắt đầu bằng / hoặc ~/ không được kết hợp với các mẫu định tuyến được áp dụng cho controller.

Xem Mức độ ưu tiên của mẫu định tuyến để biết thông tin về lựa chọn mẫu định tuyến.

Tên định tuyến dành riêng

Các từ khóa sau đây là tên tham số định tuyến dành riêng khi sử dụng Controllers hoặc Razor Pages:

  • action
  • area
  • controller
  • handler
  • page

Sử dụng page làm tham số định tuyến với định tuyến attribute là một lỗi phổ biến. Làm như vậy sẽ dẫn đến hành vi không nhất quán và khó hiểu khi tạo URL.

public class MyDemo2Controller : Controller
{
    [Route("/articles/{page}")]
    public IActionResult ListArticles(int page)
    {
        return ControllerContext.MyDisplayRouteInfo(page);
    }
}

Tên tham số đặc biệt được quá trình tạo URL sử dụng để xác định xem hoạt động tạo URL có đề cập đến Razor Pages hay Controllers hay không.

Các từ khóa sau được dành riêng trong ngữ cảnh của view Razor hoặc Razor Page:

  • page
  • using
  • namespace
  • inject
  • section
  • inherits
  • model
  • addTagHelper
  • removeTagHelper

Không nên sử dụng những từ khóa này để tạo liên kết, tham số ràng buộc model hoặc property cấp cao nhất.

Các mẫu HTTP verb

ASP.NET Core có các mẫu HTTP verb sau:

Mẫu định tuyến

ASP.NET Core có các mẫu định tuyến sau:

Định tuyến attribute với các attribute Http verb

Hãy xem xét controller sau:

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

Trong đoạn code trên:

  • Mỗi action chứa attribute [HttpGet] chỉ hạn chế khớp với các yêu cầu HTTP GET.
  • Action GetProduct bao gồm mẫu "{id}", do đó id được thêm vào mẫu "api/[controller]" trên controller. Mẫu phương thức là "api/[controller]/{id}". Do đó action này chỉ khớp với các yêu cầu GET cho dạng /api/test2/xyz, /api/test2/123, /api/test2/{any string}, v.v.
    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
       return ControllerContext.MyDisplayRouteInfo(id);
    }
    
  • Action GetIntProduct có chứa mẫu "int/{id:int}". Phần :int của mẫu sẽ ràng buộc id các giá trị định tuyến thành các chuỗi có thể được chuyển đổi thành số nguyên. Một yêu cầu GET tới  /api/test2/int/abc sẽ:
    • Không phù hợp với action này.
    • Trả về lỗi 404 Not Found.
      [HttpGet("int/{id:int}")] // GET /api/test2/int/3
      public IActionResult GetIntProduct(int id)
      {
          return ControllerContext.MyDisplayRouteInfo(id);
      }
      
  • Action GetInt2Product có {id} trong mẫu nhưng không giới hạn id các giá trị có thể được chuyển đổi thành số nguyên. Một yêu cầu GET tới /api/test2/int2/abc sẽ:
    • Phù hợp với định tuyến này.
    • Liên kết model không thể chuyển đổi abc thành số nguyên. Tham số id của phương thức là số nguyên.
    • Trả về 400 Bad Request vì liên kết model không thể chuyển đổi abc thành số nguyên.
      [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
      public IActionResult GetInt2Product(int id)
      {
          return ControllerContext.MyDisplayRouteInfo(id);
      }
      

Định tuyến attribute có thể sử dụng các attribute HttpMethodAttribution như HttpPostAttributionHttpPutAttribution và HttpDeleteAttribution. Tất cả  các attribute HTTP verb đều chấp nhận mẫu định tuyến. Ví dụ sau đây cho thấy hai action khớp với cùng một mẫu định tuyến:

[ApiController]
public class MyProductsController : ControllerBase
{
    [HttpGet("/products3")]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpPost("/products3")]
    public IActionResult CreateProduct(MyProduct myProduct)
    {
        return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
    }
}

Sử dụng đường dẫn URL /products3:

  • Action MyProductsController.ListProducts sẽ chạy khi HTTP Verb là GET.
  • Action MyProductsController.CreateProduct sẽ chạy khi HTTP Verb là POST.

Khi xây dựng API REST, hiếm khi bạn cần sử dụng [Route(...)] trên một phương thức action vì action đó chấp nhận tất cả các phương thức HTTP. Tốt hơn hết bạn nên sử dụng thuộc tính động từ HTTP verb attribute cụ thể hơn để biết chính xác những gì API của bạn hỗ trợ. Các client của API REST phải biết đường dẫn và HTTP verb nào ánh xạ tới các hoạt động logic cụ thể.

API REST nên sử dụng định tuyến attribute để model hóa chức năng của ứng dụng dưới dạng một tập hợp tài nguyên trong đó các hoạt động được biểu thị bằng HTTP verb. Điều này có nghĩa là nhiều thao tác, chẳng hạn như GET và POST trên cùng một tài nguyên logic sử dụng cùng một URL. Định tuyến attribute cung cấp mức độ kiểm soát cần thiết để thiết kế cẩn thận bố cục điểm cuối công khai của API.

Vì một định tuyến attribute áp dụng cho một action cụ thể nên thật dễ dàng để tạo các tham số bắt buộc như một phần của định nghĩa mẫu định tuyến. Trong ví dụ sau, id được yêu cầu như một phần của đường dẫn URL:

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

Action Products2ApiController.GetProduct(int):

  • Được chạy với đường dẫn URL như /products2/3
  • Không chạy với đường dẫn URL /products2.

Attribute [Consumes] cho phép một action giới hạn các loại nội dung yêu cầu được hỗ trợ. Để biết thêm thông tin, hãy xem Xác định loại nội dung yêu cầu được hỗ trợ bằng attribute Consume.

Xem Định tuyến để biết mô tả đầy đủ về mẫu định tuyến và các tùy chọn liên quan.

Để biết thêm thông tin về [ApiController], hãy xem attribute ApiController.

Tên định tuyến

Đoạn mã sau xác định tên định tuyến Products_List:

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

Tên định tuyến có thể được sử dụng để tạo URL dựa trên một định tuyến cụ thể. Tên định tuyến:

  • Không có tác động đến hoạt động định tuyến phù hợp với URL.
  • Chỉ được sử dụng để tạo URL.

Tên định tuyến phải là duy nhất cho toàn bộ ứng dụng.

Ngược lại với đoạn code trên với định tuyến mặc định thông thường, trong đó định nghĩa tham số id là tùy chọn ({id?}). Khả năng chỉ định chính xác các API có những ưu điểm, chẳng hạn như cho phép  /products và /products/5 gửi đến các action khác nhau.

Kết hợp các định tuyến attribute

Để làm cho việc định tuyến attribute ít lặp lại hơn, các attribute định tuyến trên controller được kết hợp với các attribtue định tuyến trên các action riêng lẻ. Bất kỳ mẫu định tuyến nào được xác định trên controller đều được thêm vào các mẫu định tuyến trên các action. Việc đặt attribute định tuyến trên controller sẽ làm cho tất cả các action trong controller sử dụng định tuyến attribute.

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

Trong ví dụ trên:

  • Đường dẫn URL /products có thể khớp ProductsApi.ListProducts
  • Đường dẫn URL /products/5 có thể khớp với ProductsApi.GetProduct(int).

Cả hai action này chỉ khớp với HTTP GET vì chúng được đánh dấu bằng attribute [HttpGet].

Các mẫu định tuyến được áp dụng cho một action bắt đầu bằng / hoặc ~/ không được kết hợp với các mẫu định tuyến được áp dụng cho controller. Ví dụ sau khớp với một tập hợp các đường dẫn URL tương tự như định tuyến mặc định.

[Route("Home")]
public class HomeController : Controller
{
    [Route("")]
    [Route("Index")]
    [Route("/")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("About")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

Bảng sau giải thích các attribute [Route] trong đoạn code trên:

Attribute Kết hợp với [Route("Home")] Xác định mẫu định tuyến
[Route("")] Yes "Home"
[Route("Index")] Yes "Home/Index"
[Route("/")] No ""
[Route("About")] Yes "Home/About"

Thứ tự định tuyến attribute

Định tuyến xây dựng một cây và khớp tất cả các điểm cuối cùng một lúc:

  • Các mục định tuyến hoạt động như thể được đặt theo một thứ tự lý tưởng.
  • Các định tuyến cụ thể nhất có cơ hội thực hiện trước các định tuyến tổng quát hơn.

Ví dụ: định tuyến attribute như blog/search/{topic} cụ thể hơn định tuyến attribute như blog/{*article}. Theo mặc định, định tuyến blog/search/{topic} có mức độ ưu tiên cao hơn vì nó cụ thể hơn. Sử dụng định tuyến thông thường, nhà phát triển chịu trách nhiệm đặt các định tuyến theo thứ tự mong muốn.

Các định tuyến attribute có thể định cấu hình một thứ tự bằng property Order. Tất cả các attribute định tuyến được cung cấp trong framework đều bao gồm Order. Các định tuyến được xử lý theo thứ tự tăng dần của property Order. Thứ tự mặc định là 0. Đặt định tuyến bằng cách sử dụng Order = -1 các lần chạy trước các định tuyến không đặt thứ tự. Thiết lập định tuyến bằng cách sử dụng Order = 1 các lượt chạy sau khi sắp xếp định tuyến mặc định.

Tránh phụ thuộc vào Order. Nếu không gian URL của ứng dụng yêu cầu các giá trị thứ tự rõ ràng để định tuyến chính xác thì điều đó cũng có thể gây nhầm lẫn cho khách hàng. Nói chung, định tuyến attribute chọn định tuyến chính xác bằng cách khớp URL. Nếu thứ tự mặc định được sử dụng để tạo URL không hoạt động thì việc sử dụng tên định tuyến làm ghi đè thường đơn giản hơn việc áp dụng property Order.

Hãy xem xét hai controller sau đây, cả hai đều định nghĩa định tuyến phù hợp /home:

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);
    }
}
public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

Việc yêu cầu /home bằng đoạn code trên sẽ đưa ra một ngoại lệ tương tự như sau:

AmbiguousMatchException: The request matched multiple endpoints. Matches:

 WebMvcRouting.Controllers.HomeController.Index
 WebMvcRouting.Controllers.MyDemoController.MyIndex

Việc thêm Order vào một trong các attribute định tuyến sẽ giải quyết được sự mơ hồ:

[Route("")]
[Route("Home", Order = 2)]
[Route("Home/MyIndex")]
public IActionResult MyIndex()
{
    return ControllerContext.MyDisplayRouteInfo();
}

Với đoạn code trên, /home sẽ chạy endpoint HomeController.Index. Để có được MyDemoController.MyIndex, thì request là /home/MyIndex. Ghi chú:

  • Đoạn code trên là một ví dụ hoặc thiết kế định tuyến kém. Nó được sử dụng để minh họa property Order.
  • Property Order chỉ giải quyết sự mơ hồ, mẫu đó không thể khớp được. Sẽ tốt hơn nếu loại bỏ mẫu [Route("Home")].

Xem quy ước ứng dụng và định tuyến của Razor Pages: Thứ tự định tuyến để biết thông tin về thứ tự định tuyến với Razor Pages.

Trong một số trường hợp, lỗi HTTP 500 được trả về với các định tuyến không rõ ràng. Sử dụng tính năng ghi nhật ký để xem điểm cuối nào đã gây ra lỗi AmbiguousMatchException.

Thay thế token trong các mẫu định tuyến [controller], [action], [area]

Để thuận tiện, các định tuyến attribute hỗ trợ thay thế token bằng cách đặt mã thông báo trong dấu ngoặc vuông ([]). Các token [action][area] và [controller] được thay thế bằng các giá trị của tên action, tên area và tên controller từ action nơi định tuyến được xác định:

[Route("[controller]/[action]")]
public class Products0Controller : Controller
{
    [HttpGet]
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }


    [HttpGet("{id}")]
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

Trong đoạn code trên:

[HttpGet]
public IActionResult List()
{
    return ControllerContext.MyDisplayRouteInfo();
}
  • Khớp /Products0/List
[HttpGet("{id}")]
public IActionResult Edit(int id)
{
    return ControllerContext.MyDisplayRouteInfo(id);
}
  • Khớp /Products0/Edit/{id}

Việc thay thế token xảy ra ở bước cuối cùng trong quá trình xây dựng các định tuyến attribute. Ví dụ trên hoạt động giống như đoạn code sau:

public class Products20Controller : Controller
{
    [HttpGet("[controller]/[action]")]  // Matches '/Products20/List'
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("[controller]/[action]/{id}")]   // Matches '/Products20/Edit/{id}'
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

Các định tuyến attribute cũng có thể được kết hợp với tính kế thừa. Điều này rất hiệu quả khi kết hợp với việc thay thế token. Việc thay thế token cũng áp dụng cho tên định tuyến được xác định bởi các định tuyến attribute. [Route("[controller]/[action]", Name="[controller]_[action]")] tạo một tên định tuyến duy nhất cho mỗi action:

[ApiController]
[Route("api/[controller]/[action]", Name = "[controller]_[action]")]
public abstract class MyBase2Controller : ControllerBase
{
}

public class Products11Controller : MyBase2Controller
{
    [HttpGet]                      // /api/products11/list
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]             //    /api/products11/edit/3
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

Để khớp với dấu phân cách thay thế mã thông báo theo nghĩa đen [ hoặc ], hãy thoát khỏi nó bằng cách lặp lại ký tự ([[ hoặc ]]).

Sử dụng biến tính tham số để tùy chỉnh thay thế token

Việc thay thế token có thể được tùy chỉnh bằng cách sử dụng biến áp tham số. Một biến tính tham số thực hiện IOutboundParameterTransformer và biến đổi giá trị của các tham số. Ví dụ: một biến tính tham số SlugifyParameterTransformer tùy chỉnh thay đổi giá trị định tuyến SubscriptionManagement thành subscription-management:

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

RouteTokenTransformerConvention là một quy ước mô hình ứng dụng:

  • Áp dụng một biến tính tham số cho tất cả các định tuyến attribute trong một ứng dụng.
  • Tùy chỉnh các giá trị mã thông báo định tuyến attribute khi chúng được thay thế.
public class SubscriptionManagementController : Controller
{
    [HttpGet("[controller]/[action]")]
    public IActionResult ListAll()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

Phương thức ListAll ở trên khớp với /subscription-management/list-all.

RouteTokenTransformerConvention được đăng ký dưới dạng tùy chọn:

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();

Xem tài liệu web MDN về Slug để biết định nghĩa về Slug.

Cảnh báo

Khi sử dụng System.Text.RegularExpressions để xử lý dữ liệu đầu vào không đáng tin cậy, hãy truyền thời gian chờ (timeout). Người dùng độc hại có thể cung cấp thông tin đầu vào để RegularExpressions gây ra cuộc tấn công Từ chối dịch vụ. API framework ASP.NET Core sử dụng RegularExpressions sẽ truyền timeout.

Nhiều định tuyến attribute

Định tuyến attribute hỗ trợ việc định nghĩa nhiều định tuyến để đạt được cùng một action. Cách sử dụng phổ biến nhất của việc này là bắt chước hành vi của định tuyến thông thường mặc định như trong ví dụ sau:

[Route("[controller]")]
public class Products13Controller : Controller
{
    [Route("")]     // Matches 'Products13'
    [Route("Index")] // Matches 'Products13/Index'
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

Việc đặt nhiều attribute định tuyến trên controller có nghĩa là mỗi attribute kết hợp với từng attribute định tuyến trên các phương thức action:

[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
    [HttpPost("Buy")]       // Matches 'Products6/Buy' and 'Store/Buy'
    [HttpPost("Checkout")]  // Matches 'Products6/Checkout' and 'Store/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

Tất cả các ràng buộc định tuyến HTTP verb đều thực thi interface IActionConstraint.

Khi nhiều attribute định tuyến thực thi interface IActionConstraint được đặt trên một action:

  • Mỗi ràng buộc action kết hợp với mẫu định tuyến được áp dụng cho controller.
[Route("api/[controller]")]
public class Products7Controller : ControllerBase
{
    [HttpPut("Buy")]        // Matches PUT 'api/Products7/Buy'
    [HttpPost("Checkout")]  // Matches POST 'api/Products7/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

Việc sử dụng nhiều định tuyến cho các action có vẻ hữu ích và hiệu quả, nhưng tốt hơn hết bạn nên giữ không gian URL của ứng dụng ở mức cơ bản và được xác định rõ ràng. Chỉ sử dụng nhiều định tuyến cho các action khi cần, chẳng hạn như để hỗ trợ các khách hàng hiện tại.

Chỉ định các tham số tùy chọn, giá trị mặc định và ràng buộc của định tuyến attribute

Các định tuyến attribute hỗ trợ cú pháp nội tuyến giống như các định tuyến thông thường để chỉ định các tham số tùy chọn, giá trị mặc định và các ràng buộc.

public class Products14Controller : Controller
{
    [HttpPost("product14/{id:int}")]
    public IActionResult ShowProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

Trong đoạn code trên, [HttpPost("product14/{id:int}")] áp dụng ràng buộc định tuyến. Action Products14Controller.ShowProduct chỉ được khớp với các đường dẫn URL như /product14/3. Phần mẫu định tuyến ràng buộc phân đoạn {id:int} chỉ là số nguyên.

Xem Tham khảo mẫu định tuyến để biết mô tả chi tiết về cú pháp mẫu định tuyến.

Attribute định tuyến tùy chỉnh sử dụng IRouteTemplateProvider

Tất cả các attribute định tuyến đều thực thị interface IRouteTemplateProvider. Thời gian chạy ASP.NET Core:

  • Tìm kiếm các attribute trên các lớp controller và phương thức action khi ứng dụng khởi động.
  • Sử dụng các attribute triển khai IRouteTemplateProvider để xây dựng tập hợp các định tuyến ban đầu.

Thực thi interface IRouteTemplateProvider để xác định các attribute định tuyến tùy chỉnh. Mỗi IRouteTemplateProvider cho phép bạn xác định một định tuyến duy nhất với mẫu, thứ tự và tên định tuyến tùy chỉnh:

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

Phương thức Get ở trên trả về Order = 2, Template = api/MyTestApi.

Sử dụng model ứng dụng để tùy chỉnh các định tuyến attribute

Model ứng dụng:

  • Là một model đối tượng được tạo khi khởi động trong Program.cs.
  • Chứa tất cả siêu dữ liệu được ASP.NET Core sử dụng để định tuyến và thực thi các action trong ứng dụng.

Mô hình ứng dụng bao gồm tất cả dữ liệu được thu thập từ các attribute định tuyến. Dữ liệu từ các attribute định tuyến được cung cấp khi thực thi IRouteTemplateProvider. Quy ước:

  • Có thể được viết để sửa đổi mô hình ứng dụng nhằm tùy chỉnh cách hoạt động định tuyến.
  • Được đọc khi khởi động ứng dụng.

Phần này trình bày một ví dụ cơ bản về việc tùy chỉnh định tuyến bằng model ứng dụng. Đoạn code sau làm cho các định tuyến gần như thẳng hàng với cấu trúc thư mục của dự án.

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

Đoạn code sau ngăn không cho namespace áp dụng quy ước cho các controller được định tuyến attribute:

public void Apply(ControllerModel controller)
{
    var hasRouteAttributes = controller.Selectors.Any(selector =>
                                            selector.AttributeRouteModel != null);
    if (hasRouteAttributes)
    {
        return;
    }

Ví dụ: controller sau không sử dụng NamespaceRoutingConvention:

[Route("[controller]/[action]/{id?}")]
public class ManagersController : Controller
{
    // /managers/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        return Content($"Index- template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}

Phương thức NamespaceRoutingConvention.Apply:

  • Không có gì nếu controller được định tuyến attribbute.
  • Đặt mẫu controller dựa trên namespace, với phần cơ sở namespace đã bị loại bỏ.

NamespaceRoutingConvention có thể được áp dụng 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();

Ví dụ: hãy xem xét controller sau:

using Microsoft.AspNetCore.Mvc;

namespace My.Application.Admin.Controllers
{
    public class UsersController : Controller
    {
        // GET /admin/controllers/users/index
        public IActionResult Index()
        {
            var fullname = typeof(UsersController).FullName;
            var template = 
                ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
            var path = Request.Path.Value;

            return Content($"Path: {path} fullname: {fullname}  template:{template}");
        }

        public IActionResult List(int? id)
        {
            var path = Request.Path.Value;
            return Content($"Path: {path} ID:{id}");
        }
    }
}

Trong đoạn code trên:

  • namespace cơ sở là My.Application.
  • Tên đầy đủ của controller trên là My.Application.Admin.Controllers.UsersController.
  • NamespaceRoutingConvention đặt mẫu controller thành Admin/Controllers/Users/[action]/{id?.

NamespaceRoutingConvention cũng có thể được áp dụng như một thuộc tính trên controller:

[NamespaceRoutingConvention("My.Application")]
public class TestController : Controller
{
    // /admin/controllers/test/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        var actionname = ControllerContext.ActionDescriptor.ActionName;
        return Content($"Action- {actionname} template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}

Định tuyến hỗn hợp: Định tuyến attribute và định tuyến thông thường

Các ứng dụng ASP.NET Core có thể kết hợp việc sử dụng định tuyến thông thường và định tuyến attribute. Thông thường, việc sử dụng các định tuyến thông thường cho các controller phân phát các trang HTML cho trình duyệt và định tuyến attribute cho các controller phân phát các API REST.

Các action được định tuyến thông thường hoặc định tuyến attribute. Việc đặt một định tuyến trên controller hoặc action sẽ làm cho attribute của nó được định tuyến. Các action xác định các định tuyến attribute không thể đạt được thông qua các định tuyến thông thường và ngược lại. Bất kỳ attribute định tuyến nào trên controller đều làm cho tất cả các action trong attribute controller được định tuyến.

Định tuyến attribute và định tuyến thông thường sử dụng cùng một công cụ định tuyến.

Định tuyến với các ký tự đặc biệt

Định tuyến bằng các ký tự đặc biệt có thể dẫn đến kết quả không mong muốn. Ví dụ: hãy xem xét một controller có phương thức action sau:

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

Khi string id chứa các giá trị được mã hóa sau, kết quả không mong muốn có thể xảy ra:

ASCII Đã mã hóa
/ %2F
  +

Các tham số định tuyến không phải lúc nào cũng được giải mã URL. Vấn đề này có thể được giải quyết trong tương lai. Để biết thêm thông tin, hãy xem vấn đề GitHub;

Giá trị tạo URL và môi trường xung quanh

Ứng dụng có thể sử dụng tính năng tạo URL định tuyến để tạo liên kết URL tới action. Việc tạo URL sẽ loại bỏ các URL mã hóa cứng, làm cho code trở nên mạnh mẽ hơn và dễ bảo trì hơn. Phần này tập trung vào các tính năng tạo URL do MVC cung cấp và chỉ đề cập đến những điều cơ bản về cách hoạt động của việc tạo URL. Xem Định tuyến để biết mô tả chi tiết về việc tạo URL.

Interface IUrlHelper là thành phần cơ bản của cơ sở hạ tầng giữa MVC và định tuyến để tạo URL. Một thể hiện của IUrlHelper có sẵn thông qua thuộc tính Url trong controller, view và view component.

Trong ví dụ sau, interface IUrlHelper được sử dụng thông qua property Controller.Url để tạo URL tới một action khác.

public class UrlGenerationController : Controller
{
    public IActionResult Source()
    {
        // Generates /UrlGeneration/Destination
        var url = Url.Action("Destination");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    public IActionResult Destination()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

Nếu ứng dụng đang sử dụng định tuyến thông thường mặc định thì giá trị của biến url là chuỗi đường dẫn URL /UrlGeneration/Destination. Đường dẫn URL này được tạo bằng cách định tuyến bằng cách kết hợp:

  • Các giá trị định tuyến từ yêu cầu hiện tại, được gọi là giá trị xung quanh.
  • Các giá trị được truyền tới Url.Action và thay thế các giá trị đó vào mẫu định tuyến:
ambient values: { controller = "UrlGeneration", action = "Source" }
values passed to Url.Action: { controller = "UrlGeneration", action = "Destination" }
route template: {controller}/{action}/{id?}

result: /UrlGeneration/Destination

Mỗi tham số định tuyến trong mẫu định tuyến có giá trị được thay thế bằng cách khớp tên với các giá trị và giá trị xung quanh. Tham số định tuyến không có giá trị có thể:

  • Sử dụng một giá trị mặc định nếu có.
  • Được bỏ qua nếu nó là tùy chọn. Ví dụ: id từ mẫu định tuyến {controller}/{action}/{id?}.

Việc tạo URL không thành công nếu bất kỳ tham số định tuyến bắt buộc nào không có giá trị tương ứng. Nếu việc tạo URL cho một định tuyến không thành công thì định tuyến tiếp theo sẽ được thử cho đến khi tất cả các định tuyến đã được thử hoặc tìm thấy kết quả trùng khớp.

Ví dụ trên Url.Action giả định định tuyến thông thường. Việc tạo URL hoạt động tương tự với định tuyến attribute, mặc dù các khái niệm khác nhau. Với định tuyến thông thường:

  • Các giá trị định tuyến được sử dụng để mở rộng mẫu.
  • Các giá trị định tuyến cho controller và action thường xuất hiện trong mẫu đó. Điều này hoạt động vì các URL khớp với định tuyến tuân theo quy ước.

Ví dụ sau sử dụng định tuyến attribute:

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 trong đoạn code trên sẽ tạo ra custom/url/to/destination.

LinkGenerator đã được thêm vào ASP.NET Core 3.0 để thay thế cho IUrlHelperLinkGenerator cung cấp chức năng tương tự nhưng linh hoạt hơn. Mỗi phương thức trên IUrlHelper có một nhóm phương thức tương ứng trên LinkGenerator.

Tạo URL theo tên action

Url.ActionLinkGenerator.GetPathByAction và tất cả các tình trạng quá tải liên quan đều được thiết kế để tạo điểm cuối đích bằng cách chỉ định tên controller và tên action.

Khi sử dụng Url.Action, các giá trị định tuyến hiện tại cho controller và action được cung cấp bởi runtime:

  • Giá trị của controller và action là một phần của cả giá trị xung quanh và giá trị. Phương thức Url.Action luôn sử dụng các giá trị hiện tại của action và controller tạo đường dẫn URL dẫn đến action hiện tại.

Định tuyến cố gắng sử dụng các giá trị trong giá trị xung quanh để điền thông tin không được cung cấp khi tạo URL. Hãy xem xét một định tuyến dạng như {a}/{b}/{c}/{d} với các giá trị xung quanh { a = Alice, b = Bob, c = Carol, d = David }:

  • Định tuyến có đủ thông tin để tạo URL mà không cần bất kỳ giá trị bổ sung nào.
  • Định tuyến có đủ thông tin vì tất cả tham số định tuyến đều có giá trị.

Nếu giá trị { d = Donovan } được thêm vào thì:

  • Giá trị { d = David } bị bỏ qua.
  • Đường dẫn URL được tạo là Alice/Bob/Carol/Donovan.

Cảnh báo: Đường dẫn URL được phân cấp. Trong ví dụ trên, nếu giá trị { c = Cheryl } được thêm vào thì:

  • Cả hai giá trị { c = Carol, d = David } bị bỏ qua.
  • Không còn giá trị d và việc tạo URL không thành công.
  • Các giá trị mong muốn của c và d phải được chỉ định để tạo URL.

Bạn có thể gặp phải vấn đề này với định tuyến mặc định {controller}/{action}/{id?}. Vấn đề này hiếm gặp trong thực tế vì Url.Action luôn chỉ định rõ ràng giá trị controller và  action.

Một số tình trạng quá tải của Url.Action lấy một đối tượng giá trị định tuyến để cung cấp các giá trị cho các tham số định tuyến khác ngoài controller và action. Đối tượng giá trị định tuyến thường được sử dụng với id. Ví dụ: Url.Action("Buy", "Products", new { id = 17 }). Đối tượng giá trị định tuyến:

  • Theo quy ước thường là một đối tượng thuộc loại ẩn danh.
  • Có thể là một IDictionary<> hoặc một POCO.

Bất kỳ giá trị định tuyến bổ sung nào không khớp với tham số định tuyến đều được đưa vào chuỗi truy vấn.

public IActionResult Index()
{
    var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
    return Content(url!);
}

Đoạn code trên tạo ra /Products/Buy/17?color=red.

Đoạn code sau tạo ra một URL tuyệt đối:

public IActionResult Index2()
{
    var url = Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme);
    // Returns https://localhost:5001/Products/Buy/17
    return Content(url!);
}

Để tạo một URL tuyệt đối, hãy sử dụng một trong những cách sau:

  • Một overload chấp nhận một protocol. Ví dụ như đoạn code ở trên.
  • LinkGenerator.GetUriByAction, tạo URI tuyệt đối theo mặc định.

Tạo URL theo định tuyến

Đoạn code trên đã minh họa việc tạo URL bằng cách truyền vào controller và tên action. IUrlHelper cũng cung cấp họ phương thức Url.RouteUrl. Các phương thức này tương tự như Url.Action nhưng chúng không sao chép các giá trị hiện tại của action và controller vào các giá trị định tuyến. Cách sử dụng phổ biến nhất của Url.RouteUrl:

  • Chỉ định tên định tuyến để tạo URL.
  • Nói chung không chỉ định controller hoặc tên action.
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();
    }

File Razor sau đây tạo liên kết HTML tới Destination_Route:

<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

IHtmlHelper cung cấp các phương thức HtmlHelper Html.BeginForm và Html.ActionLink để tạo các phần tử tương ứng <form> và <a>. Các phương thức này sử dụng phương thức Url.Action để tạo URL và chúng chấp nhận các đối số tương tự. Những đồng hành Url.RouteUrl cho HtmlHelper là Html.BeginRouteForm và Html.RouteLink có chức năng tương tự.

TagHelpers tạo URL thông qua TagHelper form và TagHelper <a>. Cả hai đều sử dụng IUrlHelper để thực hiện. Xem Tag Helper trong form để biết thêm thông tin.

Bên trong các view, IUrlHelper có sẵn thông qua property Url cho mọi hoạt động tạo URL đặc biệt không được đề cập ở trên.

Tạo URL trong Action Result

Các ví dụ trên cho thấy việc sử dụng IUrlHelper trong controller. Cách sử dụng phổ biến nhất trong controller là tạo URL như một phần của action result.

Các lớp cơ sở ControllerBase và Controller cung cấp các phương thức tiện lợi cho các action result tham chiếu đến một action khác. Một cách sử dụng điển hình là chuyển hướng sau khi chấp nhận đầu vào của người dùng:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int id, Customer customer)
{
    if (ModelState.IsValid)
    {
        // Update DB with new details.
        ViewData["Message"] = $"Successful edit of customer {id}";
        return RedirectToAction("Index");
    }
    return View(customer);
}

Các phương thức factory action result như RedirectToAction và CreatedAtAction tuân theo mẫu tương tự như các phương thức trên IUrlHelper.

Trường hợp đặc biệt cho các định tuyến thông thường chuyên dụng

Định tuyến thông thường có thể sử dụng một loại định nghĩa định tuyến đặc biệt gọi là định tuyến thông thường chuyên dụng. Trong ví dụ sau, định tuyến có tên blog là định tuyến thông thường chuyên dụng:

app.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");

Sử dụng các định nghĩa định tuyến ở trên, Url.Action("Index", "Home") sẽ tạo đường dẫn URL / bằng định tuyến default đó, nhưng tại sao? Bạn có thể đoán các giá trị định tuyến { controller = Home, action = Index } sẽ đủ để tạo một URL bằng cách sử dụng blog, và kết quả sẽ là /blog?action=Index&controller=Home.

Các định tuyến thông thường chuyên dụng dựa trên hành vi đặc biệt của các giá trị mặc định không có tham số định tuyến tương ứng để ngăn chặn định tuyến quá tham lam khi tạo URL. Trong trường hợp này, các giá trị mặc định là { controller = Blog, action = Article }, và không controller hãy action nào xuất hiện dưới dạng tham số định. Khi định tuyến thực hiện việc tạo URL, các giá trị được cung cấp phải khớp với các giá trị mặc định. Việc tạo URL blog không thành công vì các giá trị { controller = Home, action = Index } không khớp với { controller = Blog, action = Article }. Sau đó định tuyến quay lại thử default và thành công.

Area

Area là một tính năng MVC được sử dụng để sắp xếp các chức năng liên quan thành một nhóm riêng biệt:

  • Namespace định tuyến cho các action của controller.
  • Cấu trúc thư mục cho các view.

Việc sử dụng các area cho phép ứng dụng có nhiều controller có cùng tên, miễn là chúng có các area khác nhau. Việc sử dụng các area sẽ tạo ra một hệ thống phân cấp nhằm mục đích định tuyến bằng cách thêm một tham số định tuyến khác là area vào controller và action. Phần này sẽ thảo luận về cách định tuyến tương tác với các area. Xem Area để biết chi tiết về cách các area được sử dụng với view.

Ví dụ sau định cấu hình MVC để sử dụng định tuyến thông thường mặc định và một định tuyến area cho một area tên Blog:

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();

Trong đoạn code trên, MapAreaControllerRoute được gọi để tạo file "blog_route". Tham số thứ hai, "Blog", là tên area.

Khi khớp một đường dẫn URL như /Manage/Users/AddUser, thì định tuyến "blog_route" sẽ tạo ra các giá trị định tuyến { area = Blog, controller = Users, action = AddUser }. Giá trị định tuyến area được tạo bởi giá trị mặc định cho area. Định tuyến được tạo bởi MapAreaControllerRoute tương đương như sau:

app.MapControllerRoute("blog_route", "Manage/{controller}/{action}/{id?}",
        defaults: new { area = "Blog" }, constraints: new { area = "Blog" });
app.MapControllerRoute("default_route", "{controller}/{action}/{id?}");

MapAreaControllerRoute tạo một định tuyến bằng cách sử dụng cả giá trị mặc định và ràng buộc để area sử dụng tên khu vực được cung cấp, trong trường hợp này là Blog. Giá trị mặc định đảm bảo rằng định tuyến luôn tạo ra { area = Blog, ... }, ràng buộc yêu cầu giá trị { area = Blog, ... } để tạo URL.

Định tuyến thông thường phụ thuộc vào thứ tự. Nói chung, các định tuyến có area nên được đặt sớm hơn vì chúng cụ thể hơn các định tuyến không có area.

Sử dụng ví dụ trên, các giá trị định tuyến { area = Blog, controller = Users, action = AddUser } khớp với action sau:

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

Attribute [Area] là attribute biểu thị controller là một phần của area. Controller này nằm trong area Blog. Controller không có attribute [Area] không phải là thành viên của bất kỳ khu vực nào và không khớp khi giá trị định tuyến area được cung cấp bởi định tuyến. Trong ví dụ sau, chỉ controller đầu tiên được liệt kê mới có thể khớp với các giá trị định tuyến { area = Blog, controller = Users, action = AddUser }.

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}");
        }        
    }
}
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace2
{
    // Matches { area = Zebra, controller = Users, action = AddUser }
    [Area("Zebra")]
    public class UsersController : Controller
    {
        // GET /zebra/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}");
        }        
    }
}
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace3
{
    // Matches { area = string.Empty, controller = Users, action = AddUser }
    // Matches { area = null, controller = Users, action = AddUser }
    // Matches { 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}");
        }
    }
}

Namespace của mỗi controller được hiển thị ở đây để hoàn chỉnh. Nếu các controller ở trên sử dụng cùng một namespace thì lỗi trình biên dịch sẽ được tạo ra. Namespace lớp không ảnh hưởng đến việc định tuyến của MVC.

Hai controller đầu tiên là thành viên của các area và chỉ khớp khi tên area tương ứng của chúng được cung cấp bởi giá trị route area. Controller thứ ba không phải là thành viên của bất kỳ area nào và chỉ có thể khớp khi không có giá trị nào cho area được cung cấp bởi định tuyến.

Về mặt khớp no value, việc không có giá trị area cũng giống như khi giá trị area là null hoặc chuỗi trống.

Khi thực hiện một action bên trong một area, thì giá trị định tuyến area sẽ có sẵn dưới dạng giá trị xung quanh để định tuyến sử dụng cho việc tạo URL. Điều này có nghĩa là theo mặc định, các area hoạt động cố định để tạo URL như được minh họa bằng mẫu sau.

app.MapAreaControllerRoute(name: "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()
        {
            // Uses the 'ambient' value of area.
            var url = Url.Action("Index", "Home");
            // Returns /Manage/Home/Index
            return Content(url);
        }

        // GET /Manage/users/GenerateURLOutsideOfArea
        public IActionResult GenerateURLOutsideOfArea()
        {
            // Uses the empty value for area.
            var url = Url.Action("Index", "Home", new { area = "" });
            // Returns /Manage
            return Content(url);
        }
    }
}

Đoạn code sau tạo một URL tới /Zebra/Users/AddUser:

public class HomeController : Controller
{
    public IActionResult About()
    {
        var url = Url.Action("AddUser", "Users", new { Area = "Zebra" });
        return Content($"URL: {url}");
    }

Định nghĩa action

Các phương thức public trên controller, ngoại trừ các phương thức có attribute NonAction, đều là các action.

Code mẫu

Chẩn đoán gỡ lỗi

Để có đầu ra chẩn đoán định tuyến chi tiết, hãy đặt Logging:LogLevel:Microsoft thành Debug. Trong môi trường development, đặt cấp độ nhật ký trong appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
Nguồn: learn.microsoft.com
» Tiếp: Triển khai ứng dụng web ASP.NET
Khóa học qua video:
Lập trình Python All C# Lập trình C Java SQL Server PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên
Copied !!!