Web API 2 hỗ trợ một kiểu định tuyến mới, được gọi lại là attribute routing (định tuyến thông qua thuộc tính). Để có hình dung sơ lược về thuộc tính định tuyến (attribute routing), ta hãy đọc bài viết sau
Attribute Routing trong Web API 2. Trong hướng dẫn này, bạn sẽ sử dụng attribute routing để tạo ra REST API cho một tập hợp các cuốn sách (collection books). API sẽ hỗ trợ các action sau:
Tất cả các method trên chỉ là đọc dữ liệu (HTTP GET request). Về phần dữ liệu (data layer), chúng ta sẽ sử dụng Entity Framework. Các bản ghi của Book sẽ bao gồm các trường thông tin sau:
- ID - mã sách
- Title - tên sách
- Genre - thể loại
- Publication date - ngày xuất bản
- Price - gía
- Description - mô tả
- AuthorID (foreign key to an Authors table) - mã tác giả (là khoá phụ, liên quan tới bảng Authors).
Tuy nhiên hầu hết các API đều trả về tập hợp dữ liệu nhỏ bao gồm (title, author, và genre). Để lấy toàn bộ thông tin thì chỉ có request phần details /api/books/{id}/details
.
Tạo ra Visual Studio Project
Mở ứng dụng Visual Studio. Từ phần File menu, chọn New và sau đó chọn Project.
Mở rộng phần Installed > chọn Visual C# category. Dưới Visual C#, chọn Web. Trong danh sách project templates, chọn ASP.NET Web Application (.NET Framework). Đặt tên cho project là "BooksAPI".
Trong hộp thoại New ASP.NET Web Application, lựa chọn Empty template. Dưới phần "Add folders and core references for", lựa chọn Web API checkbox. Click OK.
Vậy là chúng ta đã tạo ra một skeleton project (khung project) đã có sẵn cấu hình cho các chức năng của Web API.
Domain Models
Tiếp theo, ta sẽ viết các class cho phần models. Trong Solution Explorer, click chuột phải vào thư mục Models. Chọn Add, sau đó chọn Class. Đặt tên class là Author
.
Trong Author.cs viết code như sau:
using System.ComponentModel.DataAnnotations;
namespace BooksAPI.Models
{
public class Author
{
public int AuthorId { get; set; }
[Required]
public string Name { get; set; }
}
}
Sau đó add thêm class mới tên là
Book
. Code như sau:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BooksAPI.Models
{
public class Book
{
public int BookId { get; set; }
[Required]
public string Title { get; set; }
public decimal Price { get; set; }
public string Genre { get; set; }
public DateTime PublishDate { get; set; }
public string Description { get; set; }
public int AuthorId { get; set; }
[ForeignKey("AuthorId")]
public Author Author { get; set; }
}
}
Thêm Web API Controller
Trong bước này, chúng ta sẽ add thêm Web API controller, nó sẽ sử dụng Entity Framework đóng vai trò là data layer (lớp xử lý dữ liệu).
Nhấn phím CTRL+SHIFT+B để build dự án. Entity Framework sử dụng để reflection để khám phá các thuộc tính của model, do đó nó yêu cầu một assembly đã được biên dịch để tạo ra database schema.
Trong Solution Explorer, right-click vào Controllers folder. Chọn Add, sau đó chọn Controller.
Trong hộp thoại Add Scaffold, chọn Web API 2 Controller with actions, using Entity Framework.
Trong hộp thoại Add Controller, phần Controller name, viết "BooksController". Tích chọn "Use async controller actions" checkbox. Dành cho Model class, lựa chọn "Book". (Nếu bạn không nhìn thấy Book
class được liệt kê trong dropdown, thì có lẽ bạn chưa build project, do vậy hãy rebuild lại project.) Sau đó click vào nút "+".
Click Add trong New Data Context dialog.
Click Add trong Add Controller dialog. Đặt tên cho class Controller là BooksController
nó định nghĩa API controller. Nó sẽ thêm vào một class tên BooksAPIContext
trong Models folder, nó định nghĩa data context cho Entity Framework.
Seed the Database
Từ Tools menu, chọn NuGet Package Manager, và sau đó lựa chọn Package Manager Console.
Trong cửa sổ Package Manager Console, nhập lệnh sau:
Add-Migration
Lệnh này sẽ tạo ra Migrations folder và add thêm file code mới có tên là Configuration.cs.
Mở file này và sau đó add đoạn code sau vào phương thức Configuration.Seed
.
protected override void Seed(BooksAPI.Models.BooksAPIContext context)
{
context.Authors.AddOrUpdate(new Author[] {
new Author() { AuthorId = 1, Name = "Ralls, Kim" },
new Author() { AuthorId = 2, Name = "Corets, Eva" },
new Author() { AuthorId = 3, Name = "Randall, Cynthia" },
new Author() { AuthorId = 4, Name = "Thurman, Paula" }
});
context.Books.AddOrUpdate(new Book[] {
new Book() { BookId = 1, Title= "Midnight Rain", Genre = "Fantasy",
PublishDate = new DateTime(2000, 12, 16), AuthorId = 1, Description =
"A former architect battles an evil sorceress.", Price = 14.95M },
new Book() { BookId = 2, Title = "Maeve Ascendant", Genre = "Fantasy",
PublishDate = new DateTime(2000, 11, 17), AuthorId = 2, Description =
"After the collapse of a nanotechnology society, the young" +
"survivors lay the foundation for a new society.", Price = 12.95M },
new Book() { BookId = 3, Title = "The Sundered Grail", Genre = "Fantasy",
PublishDate = new DateTime(2001, 09, 10), AuthorId = 2, Description =
"The two daughters of Maeve battle for control of England.", Price = 12.95M },
new Book() { BookId = 4, Title = "Lover Birds", Genre = "Romance",
PublishDate = new DateTime(2000, 09, 02), AuthorId = 3, Description =
"When Carla meets Paul at an ornithology conference, tempers fly.", Price = 7.99M },
new Book() { BookId = 5, Title = "Splish Splash", Genre = "Romance",
PublishDate = new DateTime(2000, 11, 02), AuthorId = 4, Description =
"A deep sea diver finds true love 20,000 leagues beneath the sea.", Price = 6.99M},
});
}
Trong cửa sổ Package Manager Console, viết lệnh sau vào:
add-migration Initial
update-database
Các câu lệnh này sẽ tạo ra local database và gọi phương thức Seed để xuất bản dữ liệu.
Thêm DTO Classes
Nếu bạn chạy ứng dụng bây giờ và gửi đi GET request /api/books/1, thì response sẽ trông có vẻ như sau. (Tôi sẽ đã thêm khoảng cách vào chút để cho dễ đọc).
{
"BookId": 1,
"Title": "Midnight Rain",
"Genre": "Fantasy",
"PublishDate": "2000-12-16T00:00:00",
"Description": "A former architect battles an evil sorceress.",
"Price": 14.95,
"AuthorId": 1,
"Author": null
}
Thay vì đó, tôi muốn yêu cầu này trả về một tập hợp con của các trường. Do đó, tôi muốn trả về tên tác giả, hơn là mã tác giả. Để làm được việc này, chúng ta sẽ chỉnh sửa controller method để trả về kết quả dưới dạng DTO (data transfer object) thay vì là EF model. Một đối tượng DTO được thiết kể chỉ để chứa dữ liệu.
Trong Solution Explorer, right-click vào project và chọn Add | New Folder. Đặt tên folder là "DTOs". Thêm class đặt tên BookDto
trong DTOs folder, cùng với đoạn code như sau:
namespace BooksAPI.DTOs
{
public class BookDto
{
public string Title { get; set; }
public string Author { get; set; }
public string Genre { get; set; }
}
}
Thêm class khác đặt tên là BookDetailDto
.
using System;
namespace BooksAPI.DTOs
{
public class BookDetailDto
{
public string Title { get; set; }
public string Genre { get; set; }
public DateTime PublishDate { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public string Author { get; set; }
}
}
Tiếp theo, cập nhật class
BooksController
để trả về đối tượng
BookDto
. Chúng ta sẽ sử dụng phương thức
Queryable.Select cho
Book
instances và
BookDto
instances. Đây là đoạn code đã update cho controller class.
using BooksAPI.DTOs;
using BooksAPI.Models;
using System;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
namespace BooksAPI.Controllers
{
public class BooksController : ApiController
{
private BooksAPIContext db = new BooksAPIContext();
private static readonly Expression<Func<Book, BookDto>> AsBookDto =
x => new BookDto
{
Title = x.Title,
Author = x.Author.Name,
Genre = x.Genre
};
public IQueryable<BookDto> GetBooks()
{
return db.Books.Include(b => b.Author).Select(AsBookDto);
}
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
BookDto book = await db.Books.Include(b => b.Author)
.Where(b => b.BookId == id)
.Select(AsBookDto)
.FirstOrDefaultAsync();
if (book == null)
{
return NotFound();
}
return Ok(book);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
Chú ý
Tôi đã xoá các phương thức PutBook
, PostBook
, và DeleteBook
, bởi vì chúng không còn cần thiết trong tutorial này.
Và bây giờ nếu bạn chạy ứng dụng và gửi đi request /api/books/1, kết quả response có thể trông như sau:
{"Title":"Midnight Rain","Author":"Ralls, Kim","Genre":"Fantasy"}
Thêm Route Attributes
Tiếp theo, chúng ta sẽ chuyển đổi sang sử dụng attribute routing (định tuyến thuộc tính). Đầu tiên, thêm thuộc tính RoutePrefix vào phần class controller. Thuộc tính này sẽ định nghĩa khởi tạo một phần URI cho tất cả các method trong controller này.
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
Sau đó lại thêm thuộc tính [Route] vào từng controller actions, như sau:
[Route("")]
public IQueryable<BookDto> GetBooks()
{
}
[Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
}
Mẫu route cho mỗi controller method sẽ thêm tiền tố cùng với chuỗi thuộc tính Route. Ví dụ với phương thức GetBook, thì route template sẽ bao gồm tham số như string "{id:int}", điều này chỉ ra rằng URI segment sẽ chứa một giá trị là số nguyên.
Get Book Details
Để lấy thông tin chi tiết cuốn sách, thì client sẽ gửi đi GET request có định dạng/api/books/{id}/details
, trong đó {id} là ID của book.
Thêm phương thức sau vào trong BooksController
class.
[Route("{id:int}/details")]
[ResponseType(typeof(BookDetailDto))]
public async Task<IHttpActionResult> GetBookDetail(int id)
{
var book = await (from b in db.Books.Include(b => b.Author)
where b.BookId == id
select new BookDetailDto
{
Title = b.Title,
Genre = b.Genre,
PublishDate = b.PublishDate,
Price = b.Price,
Description = b.Description,
Author = b.Author.Name
}).FirstOrDefaultAsync();
if (book == null)
{
return NotFound();
}
return Ok(book);
}
Nếu bạn gửi đi request /api/books/1/details
, thì kết quả trả về trông sẽ giống như sau:
{
"Title": "Midnight Rain",
"Genre": "Fantasy",
"PublishDate": "2000-12-16T00:00:00",
"Description": "A former architect battles an evil sorceress.",
"Price": 14.95,
"Author": "Ralls, Kim"
}
Get Books By Genre
Để lấy danh sách sách theo thể loại thì client sẽ gửi đi GET request là /api/books/genre
, trong đó genre là tên của thể loại sách. (For example, /api/books/fantasy
.)
Thêm phương thức sau vào trong
BooksController
.
[Route("{genre}")]
public IQueryable<BookDto> GetBooksByGenre(string genre)
{
return db.Books.Include(b => b.Author)
.Where(b => b.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase))
.Select(AsBookDto);
}
Ở đây chúng ta định nghĩa route có chứa tham số {genre} trong URI template. Chú ý rằng Web API sẽ enable cho 2 tham số để route tới các phương thức khác nhau.
/api/books/1
/api/books/fantasy
Làm được điều đó bởi vì GetBook
method bao gồm ràng buộc đó là "id" segment cần phải là số nguyên:
[Route("{id:int}")]
public BookDto GetBook(int id)
{
}
Nếu bạn gửi đi request /api/books/fantasy, thì kết quả trả về sẽ như sau
[ { "Title": "Midnight Rain", "Author": "Ralls, Kim", "Genre": "Fantasy" }, { "Title": "Maeve Ascendant", "Author": "Corets, Eva", "Genre": "Fantasy" }, { "Title": "The Sundered Grail", "Author": "Corets, Eva", "Genre": "Fantasy" } ]
Get Books By Author
Để lấy danh sách sách theo từng tác giả riêng biệt, client sẽ gửi đi GET request /api/authors/id/books
, trong đó id là ID của tác giả.
Thêm đoạn code sau vào trong class
BooksController
.
[Route("~/api/authors/{authorId:int}/books")]
public IQueryable<BookDto> GetBooksByAuthor(int authorId)
{
return db.Books.Include(b => b.Author)
.Where(b => b.AuthorId == authorId)
.Select(AsBookDto);
}
Ví dụ này rất thú vị bởi sách đang được liệt kê theo child resource đó là "authors". Mẫu này là một RESTful API phổ biến.
Dấu ngã (~) trong route template để ghi đè the route prefix trong RoutePrefix attribute.
Get Books By Publication Date
Để lấy ra danh sách sách Theo ngày xuất bản thì client sẽ gửi đi GET request to /api/books/date/yyyy-mm-dd
, trong đó yyyy-mm-dd là ngày tháng.
Đây là một trong những cách đó.
[Route("date/{pubdate:datetime}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
return db.Books.Include(b => b.Author)
.Where(b => DbFunctions.TruncateTime(b.PublishDate)
== DbFunctions.TruncateTime(pubdate))
.Select(AsBookDto);
}
Trong đó ràng buộc tham số {pubdate:datetime}
phù hợp với giá trị DateTime. Ràng buộc này hoạt động tốt thậm chí rất tốt. Ví dụ, các URI sau sẽ phù hợp với route.
/api/books/date/Thu, 01 May 2008
/api/books/date/2000-12-16T00:00:00
Không có gì sai trong URI này cả. Tuy nhiên, bạn phải tuân thủ quy tắc về cú pháp để viết ngày tháng. Nhìn định dạng URI thế kia có vẻ dễ đối với lập trình viên nhưng lại làm khó cho người dùng.
Vậy chúng ta cần khiến cho người dùng làm việc có thể dễ hơn bằng cách chúng ta thêm regular express vào trong ràng buộc. Ví dụ như sau:
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
}
Bây giờ ngày tháng chỉ cần định dạng là "yyyy-mm-dd" thì sẽ phù hợp với yêu cầu.
Ngoài ra bạn cũng có thể để URI có định dạng như sau (/api/books/date/yyyy/mm/dd
) bằng cách thêm [Route] attribute khác với biểu thức quy tắc khác.
Ví dụ như dưới đây.
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
[Route("date/{*pubdate:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")] // new
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
// ...
}
Và bây giờ reqeust này là hợp lệ ;)
/api/books/date/2013/06/17
Controller Code
Đây là toàn bộ của BooksController class.
using BooksAPI.DTOs;
using BooksAPI.Models;
using System;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
namespace BooksAPI.Controllers
{
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
private BooksAPIContext db = new BooksAPIContext();
private static readonly Expression<Func<Book, BookDto>> AsBookDto =
x => new BookDto
{
Title = x.Title,
Author = x.Author.Name,
Genre = x.Genre
};
[Route("")]
public IQueryable<BookDto> GetBooks()
{
return db.Books.Include(b => b.Author).Select(AsBookDto);
}
[Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
BookDto book = await db.Books.Include(b => b.Author)
.Where(b => b.BookId == id)
.Select(AsBookDto)
.FirstOrDefaultAsync();
if (book == null)
{
return NotFound();
}
return Ok(book);
}
[Route("{id:int}/details")]
[ResponseType(typeof(BookDetailDto))]
public async Task<IHttpActionResult> GetBookDetail(int id)
{
var book = await (from b in db.Books.Include(b => b.Author)
where b.AuthorId == id
select new BookDetailDto
{
Title = b.Title,
Genre = b.Genre,
PublishDate = b.PublishDate,
Price = b.Price,
Description = b.Description,
Author = b.Author.Name
}).FirstOrDefaultAsync();
if (book == null)
{
return NotFound();
}
return Ok(book);
}
[Route("{genre}")]
public IQueryable<BookDto> GetBooksByGenre(string genre)
{
return db.Books.Include(b => b.Author)
.Where(b => b.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase))
.Select(AsBookDto);
}
[Route("~/api/authors/{authorId}/books")]
public IQueryable<BookDto> GetBooksByAuthor(int authorId)
{
return db.Books.Include(b => b.Author)
.Where(b => b.AuthorId == authorId)
.Select(AsBookDto);
}
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
[Route("date/{*pubdate:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
return db.Books.Include(b => b.Author)
.Where(b => DbFunctions.TruncateTime(b.PublishDate)
== DbFunctions.TruncateTime(pubdate))
.Select(AsBookDto);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
Tóm Lại
Attribute routing sẽ mang cho bạn một cách mới để định tuyến tốt hơn khi bạn muốn thiết kế các URI cho API của bạn.
Đăng nhận xét