Định tuyến (Routing) là cách thức để Web API  chỉ ra đường dẫn URI sẽ trỏ tới action phù hợp trong controller. Nói một cách nôm na thì Routing chính là kẻ dẫn đường cho Web API sẽ gọi action nào tương đương với uri đã được cung cấp. Web API 2 hỗ trợ một kiểu định tuyến mới, được gọi là attribute routing (định tuyến thuộc tính). Như tên gọi đã mang một ẩn ý, attribute routing (định tuyến thuộc tính) sẽ là bộ định tuyến được xác định thông qua thuộc tính Route. Ví dụ, bạn có thể dễ dàng tạo ra URI bằng cách mô tả các đặc tính dựa trên việc thừa kế các nguồn tài nguyên. 
Kiểu định tuyến mới đây, được gọi là convention-based routing (định tuyến dựa trên quy ước), nó vẫn được hỗ trợ đầy đủ. Trong thực tế, bạn có thể kết hợp cả hai công nghệ định tuyến này trong project.  
Bài viết này sẽ trình bày cho bạn cách thức để enable định tuyến thuộc tính và mô tả các tuỳ chọn khác nhau trong định tuyến thuộc tính (attribute routing). Và để kết thúc loạt hướng dẫn về định tuyến thì bạn có thể xem bài viết chi tiết và đầy đủ về Tạo ra REST API cùng với Định Tuyến Thuộc Tính trong Web API 2

Yêu cầu cần có

Bạn phải cài đặt Visual Studio 2019  phiên bản Community, Professional, hoặc Enterprise. 
Ngoài ra, bạn cần sử dụng NuGet Package Manager để cài đặt các thư viện cần thiết. Từ menu Tools trong Visual Studio, bạn chọn NuGet Package Manager, sau đó chọn Package Managẻ Console. Viết đoạn lệnh sau vào trong cửa sổ Package Manager Consolse: 
Install-Package Microsoft.AspNet.WebApi.WebHost

Tại sao cần Định Tuyến Thuộc Tính?

Phiên bản định tuyến đầu tiên của Web API đó là dựa vào định tuyến dựa vào quy ước (convention-based routing). Trong loại định tuyến này, bạn định nghĩa một hoặc nhiều mẫu định tuyến, về cơ bản nó sẽ là chuỗi có tham số. Khi frameworks nhận được một request (yêu cầu), nó sẽ tìm ra URI tương ứng với bộ mẫu định tuyến đã có (route template). (Để hiểu  rõ hơn về định tuyến dựa trên quy ước thì bạn hãy đọc bài viết Định tuyến trong ASP.NET Web API
Một trong những ưu điểm của định tuyến dựa trên quy ước đó là cácc mẫu được định nghĩa ở một nơi và các quy tắc định tuyến được áp dụng một cách thống nhất với các controllers. Không may là, định tuyến dựa trên quy ước sẽ khiến chúng ta rất khó để hỗ trợ nhiều mẫu URI vẫn được sử dụng phổ biến trong RESTful APIs. Ví dụ, nguồn tài nguyên thường chứa các tài nguyên con: Khác hàng sẽ có nhiều đơn hàng, phim sẽ có nhiều diễn viên, sách sẽ có nhiều tác giả, và hơn thế nữa. Do đó kiểu tạo ra URI theo kiểu mẫu liên quan như sau 
/customers/1/orders
Với kiểu định URI này thì sẽ rất khó để tạo ra định tuyến bằng cách sử dụng bộ định tuyến dựa vào quy ước (convention-based routing). Mặc dù vậy bạn vẫn có thể tạo ra được bộ định tuyến đó, tuy nhiên kết quả là rất khó tuỳ biến bởi vị bạn sẽ cso quá nhiều controller hoặc kiểu tài nguyên được gọi tới . 
Với attribute routing (định tuyến thuộc tính), thì rất dễ dàng để bạn đạt được điều mong muốn như trên (tức là tạo ra uri loằng ngoằng có liên quan giữu nhiều tài nguyên). Bạn có thể thêm thuộc tính vào controller action: 
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
Sau đây là một vài ví dụ về mẫu uri khác mà định tuyến thuộc tính có thể tạo ra một cách dễ dàng 
Ví dụ api 2 có các phiên bản khác nhau (trong này là version 1 và version 2: v1 v2) . 
Trong ví dụ này là , "/api/v1/products" nó sẽ được định tuyến tới controller version 1, còn đây là api cho vesion 2: "/api/v2/products".
/api/v1/products /api/v2/products
Nạp chồng một phần URI 
Trong ví dụ này, "1" là số thứ tự của đơn hàng, nhưng "pending" thì là chỉ tới các đơn hàng đang trong trạng thái pending. 
/orders/1 /orders/pending
Ngoài ra còn nhiều loại tham số định tuyến khác 
Trong ví dụ này, "1" là số thứ tự của order, nhưng "2020/01/06" lại là ngày đặt hàng. 
/orders/1 /orders/2020/01/06

Cho Phép Định Tuyến Thuộc Tính

Để enable định tuyến thuộc tính, ta gọi MapHttpAttributeRoutes trong phần cấu hình. Phương thức mở rộng này được định nghĩa trong class System.Web.Http.HttpConfigurationExtensions. 
using System.Web.Http; namespace WebApplication { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API routes config.MapHttpAttributeRoutes(); // Other Web API configuration not shown. } } }
Định tuyến thuộc tính có thể kết hợp cùng với định tuyến quy ước. Để định nghĩa định tuyến quy ước, bạn sẽ gọi phương thức MapHttpRoute.

public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Attribute routing. config.MapHttpAttributeRoutes(); // Convention-based routing. config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } 
} 
Để có thêm thông tin về phần này thì bạn hãy đọc Web API, xem cấu hình trong Configuring ASP.NET Web API 2.
Chú ý: Tích hợp từ Web API 1 
Tới thời kỳ Web API 2, thì mẫu project Web API sinh ra code giống như sau:
protected void Application_Start()
{
    // WARNING - Not compatible with attribute routing.
    WebApiConfig.Register(GlobalConfiguration.Configuration);
}
Nếu thuộc tính định tuyến được cho phép, đoạn code này sẽ throw ra exception. Nếu bạn upgrade dự án Web API hiện tại để sử dụng định tuyến thuộc tính, thì bạn phải chắc chắn rằng bạn đã cấu hình code như sau : 
protected void Application_Start() { // Pass a delegate to the Configure method. GlobalConfiguration.Configure(WebApiConfig.Register); }
 Thêm Định Tuyến Thuộc Tính
Đây là ví dụ đã định nghĩa phần định tuyến bằng cách sử dụng thuộc tính: 
public class OrdersController : ApiController
{
    [Route("customers/{customerId}/orders")]
    [HttpGet]
    public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}
Trong đoạn code trên thì chuỗi  "customers/{customerId}/orders"  dùng để tạo ra bộ định tuyến URI. Web API sẽ cố gắng tìm kiếm URI phù hợp với template. Trong ví dụ này, "customers" và "orders" là các segments (phân khúc), và "{customerId}" là một tham số. Đoạn Uri sau sẽ phù hợp với mẫu: 
  • http://localhost/customers/1/orders
  • http://localhost/customers/thidk/orders
  • http://localhost/customers/1234-5678/orders
Bạn có thể giới hạn sự phù hợp bằng các sử dụng các constraints (ràng buộc), phần này sẽ được mô tả ngay sau bài viết này. 
Chú ý rằng tham số "{customerId}" trong mẫu định tuyến phù hợp với tên tham số là customerId trong phương thức. Khi Web API gọi tới controller action, thì bạn cố gắng bind tới tham số định tuyến. Ví dụ, nếu URI là http://example.com/customers/1/orders, Web API cố gắng gắn kết giá trị "1" cho tham số customerId trong action. 
Mẫu URI có thể có vài tham số: 
[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }
Bất kỳ một phương thức controller nào nếu không có route attribute (thuộc tính định tuyến) sẽ sử dụng bộ định tuyến dựa trên quy ước (convention-based routing). Với cách đó, bạn có thể kết hợp cả 2 loại định tuyến trong cùng một project. 

Phươc Thức HTTP

Web API cũng lựa chọn các actions dựa trên HTTP Method của request (GET, POST, vv). Mặc định, Web API sẽ khá nhạy bén với chữ bắt đầu của phương thức trong controller. Ví dụ, một controller method được đặt tên là PutCustomers sẽ phù hợp với HTTP PUT request. 
Bạn có thể ghi đè quy ước này bằng cách chỉnh sửa lại method bằng bất kỳ thuộc tính nào trong số các thuộc tính liệt kê sau đây.
  • [HttpDelete]
  • [HttpGet]
  • [HttpHead]
  • [HttpOptions]
  • [HttpPatch]
  • [HttpPost]
  • [HttpPut]
Ví dụ sau đây sẽ ánh xạ method CreateBook với HTTP POST request. 
[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }
Dành cho tất cả các method HTTP khác, bao gồm cả những methods không chuẩ, sử dụng AcceptVers thuộc tính để đưa ra danh sácch các HTTP methods. 
// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }

Tiền tố Route

Thông thường, bộ định tuyến trong controller sẽ tất cả sẽ bắt đầu cùng một tiền tố. Ví dụ: 
public class BooksController : ApiController
{
    [Route("api/books")]
    public IEnumerable<Book> GetBooks() { ... }

    [Route("api/books/{id:int}")]
    public Book GetBook(int id) { ... }

    [Route("api/books")]
    [HttpPost]
    public HttpResponseMessage CreateBook(Book book) { ... }
}
Bạn có thể set chung tiền tố cho toàn bộ controller bằng cách sử dụng thuộc tính [RoutePrefix]: 
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET api/books
    [Route("")]
    public IEnumerable<Book> Get() { ... }

    // GET api/books/5
    [Route("{id:int}")]
    public Book Get(int id) { ... }

    // POST api/books
    [Route("")]
    public HttpResponseMessage Post(Book book) { ... }
}
Sử dụng dấu ngã (~) trong thuộc tính của method để override các tiền tố định tuyến. 
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET /api/authors/1/books
    [Route("~/api/authors/{authorId:int}/books")]
    public IEnumerable<Book> GetByAuthor(int authorId) { ... }

    // ...
}
Bộ định tuyến cũng có thể bao gồm các tham số: 
[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
    // GET customers/1/orders
    [Route("orders")]
    public IEnumerable<Order> Get(int customerId) { ... }
}

Ràng Buộc Định Tuyến

Ràng buộc định tuyến (Route constraints) cho phép bạn giới hạn cách thức các tham số trong bộ định tuyến tìm được bản phù hợp. Tức là cung cấp thêm các điều kiện ràng buộc cho các tham số định tuyến, khiến bộ định tuyến có quy định chặt chẽ hơn. 
Cú phép thông thường đó là "{parameter:constraint}". Ví dụ: 

   [Route("users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("users/{name}")]
public User GetUserByName(string name) { ... }
Ở đây, bộ định tuyến đầu tiên sẽ được lựa chọn nếu đoạn "id" của URI là số nguyên. Trường hợp khác thì bộ định tuyến thứ hai sẽ được chọn. 
Bảng sau đây sẽ liệt kê ra toàn bộ các ràng buộc được hỗ trợ. 
RàngbuộcMô tảVí dụ
alphaPhù hợp với các ký tự từ a-z, A-Z kể cả chữ hoa và chữ thường. {x:alpha}
boolPhù hợp với kiểu giá trị Boolean. {x:bool}
datetimePhù hợp với kiểu dữ liệu thời gian DateTime. {x:datetime}
decimalPhù hợp với kiểu dữ liệu số decimal. {x:decimal}
doublePhù hợp với giá trị 64-bit floating-point.{x:double}
floatPhù hợp với giá trị 32-bit floating-point.{x:float}
guidPhù hợp với giá trị GUID.{x:guid}
intPhù hợp với giá trị 32-bit.{x:int}
lengthPhù hợp với chuỗi có độ dài chỉ định hoặc trong một phạm vi cho phép. Trong ví dụ trên là chuỗi có 6 ký tự, và chuỗi có độ dài nằm trong khoảng từ 1 tới 20. {x:length(6)} {x:length(1,20)}
longPhù hợp với giá trị số nguyên 64-bit.{x:long}
maxPhù hợp với giá trị số nguyên cùng với giá trị lớn nhất. {x:max(10)}
maxlengthPhù hợp với một chuỗi có độ dài tối đa. {x:maxlength(10)}
minPhù hợp với số nguyên cùng với giá trị nhỏ nhất. {x:min(10)}
minlengthPhù hợp với một chuỗi có độ dài tối đa. {x:minlength(10)}
rangePhù hợp với một số nguyên nằm trong một phạm vi nhất định. {x:range(10,50)}
regexĐưa ra biểu thức quy tắc cho tham số định tuyến. {x:regex(^\d{3}-\d{3}-\d{4}$)}
Chú ý rằng trong một số ràng buộc thì bạn cần đưa ra thêm điều kiện, ví dụ như là "min", thì giá trị của tham số bạn sẽ để trong dấu ngoặc đơn. Bạn có thê áp dụng nhiều ràng buộc dành cho một tham số, và được phân cách nhau bởi dấu hai chấm. 
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }

Tuỳ Chọn Tham Số URI Parameters và Giá Trị Mặc Định

Bạn có thể tạo ra các tham số URI tuỳ chọn bằng cách thêm các dấu hỏi (?) cho các tham số tuỳ chọn. Nếu tham số tuỳ chọn là optional (có thể có hoặc không), bạn cần định nghĩa giá trị mặc định cho tham số trong phương thức. 
public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int?}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}
Trong ví dụ này, /api/books/locale/1033 và /api/books/locale trả về cùng một nguồn tài nguyên. 
Ngoài ra, bạn cũng có thể chỉ ra giá trị mặc định bên trong mẫu route, như sau:
public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int=1033}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}

Route Names - Tên Route 

In Web API, every route has a name. Route names are useful for generating links, so that you can include a link in an HTTP response.
Trong Web API, mọi route đều có một tên. Tên route rất hữu ích trong việc sinh ra các liên kết, do đó có thể bao gồm cả linnk tới HTTP response. 
Để chỉ định cho route name, gán giá trị cho thuộc tính Name trên attribute của Route. Ví dụ sau sẽ chỉ ra cho cách bạn định nghĩa tên cho route, và nó cũng cho bạn thấy route name khi được sinh ra trong link. 
public class BooksController : ApiController
{
    [Route("api/books/{id}", Name="GetBookById")]
    public BookDto GetBook(int id) 
    {
        // Implementation not shown...
    }

    [Route("api/books")]
    public HttpResponseMessage Post(Book book)
    {
        // Validate and add book to database (not shown)

        var response = Request.CreateResponse(HttpStatusCode.Created);

        // Generate a link to the new book and set the Location header in the response.
        string uri = Url.Link("GetBookById", new { id = book.BookId });
        response.Headers.Location = new Uri(uri);
        return response;
    }
}

Route Order - Thứ Tự Route 

Khi một framwork cố gắng tìm URI phù hợp, nó sẽ đánh gía thứ tự route của các phần khác nhau. Để chỉ định thứ tự, bạn set thuộc tính Order trên route attribute. Giá trị thấp hơn sẽ được đánh giá trước. Giá trị mặc định là không. Điều đó có nghĩa là route nào có Order thấp thì sẽ được thực hiện trước. 
Trong ví dụ dưới đây sẽ chỉ ra cho bạn cách mà Route đã được quyết định thứ tự hoạt động: 

  1. 1. So sánh thuộc tính Order củ route attribute 
    2. Xem xét xem các phân khúc URI trong route template. Với mỗi phân khúc, thứ tự sẽ như sau: 
    a. Ký tự phân khúc 
    b. Tham số có ràng buộc route 
    c. Tham số không có ràng buộc route 
    d. Tham số wildcard có ràng buộc
    e. Tham số wildcard không có ràng buộc 

    3. Trong một số trường hợp, bộ định tuyến đượ đặt Theo việc so sánh chuỗi trong route template. 
Here is an example. Suppose you define the following controller:
C#
[RoutePrefix("orders")]
public class OrdersController : ApiController
{
    [Route("{id:int}")] // constrained parameter
    public HttpResponseMessage Get(int id) { ... }

    [Route("details")]  // literal
    public HttpResponseMessage GetDetails() { ... }

    [Route("pending", RouteOrder = 1)]
    public HttpResponseMessage GetPending() { ... }

    [Route("{customerName}")]  // unconstrained parameter
    public HttpResponseMessage GetByCustomer(string customerName) { ... }

    [Route("{*date:datetime}")]  // wildcard
    public HttpResponseMessage Get(DateTime date) { ... }
}
Và kết quả là thứ tự route sẽ được đặt như sau
  1. orders/details
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. orders/pending
Notice that "details" is a literal segment and appears before "{id}", but "pending" appears last because the Order property is 1. (This example assumes there are no customers named "details" or "pending". In general, try to avoid ambiguous routes. In this example, a better route template for GetByCustomer is "customers/{customerName}" )
Chú ý rằng "details" là một phân khúc và nó xuất hiện trước "{id}", nhưng "pending" xuất hiện cuối cùng bởi vì thuộc tính RouteOrder = 1. Điều này khiến cho chúng ta hiểu rằng với customer được đặt tên là details hay pending cũng vậy. Ngoài ra, ta cố gắng tránh việc có nhiều route trung nhau và khó phân biệt. Trong ví dụ này, thì một route tốt ví dụ như GetByCusstomer đó là "customers/{customerName}". 

Kết Luận 

Trong bài viết này bạn đã có một cái nhìn khá tổng quan về đầy đủ về route. Route là một phần trọng trong API nói riêng và ASP.NET  nói chung. Trong bài viết tiếp theo tôi sẽ mô tả chi tiết hơn về phần thuộc tính route và các loại. 

Mọi câu hỏi xin mời bạn post trong group Facebook CodeLean Community. 

Post a Comment

Mới hơn Cũ hơn