在 ASP.NET Web API 2 中使用屬性路由建立 REST API
Web API 2 支援新的路由類型,稱為 屬性路由。 如需屬性路由的一般概觀,請參閱 Web API 2 中的屬性路由。 在本教學課程中,您將使用屬性路由來建立書籍集合的 REST API。 API 將支援下列動作:
動作 | 範例 URI |
---|---|
取得所有書籍的清單。 | /api/books |
依識別碼取得書籍。 | /api/books/1 |
取得書籍的詳細資料。 | /api/books/1/details |
依內容類型取得書籍清單。 | /api/books/my |
依發行日期取得書籍清單。 | /api/books/date/2013-02-16 /api/books/date/2013/02/16 (替代表單) |
取得特定作者的書籍清單。 | /api/authors/1/books |
所有方法都是唯讀 (HTTP GET 要求) 。
針對資料層,我們將使用 Entity Framework。 書籍記錄會有下欄欄位:
- 識別碼
- 標題
- Genre
- 發行日期
- 價格
- 描述
- AuthorID (Author 資料表的外鍵)
不過,對於大部分的要求,API 會傳回此資料的子集, (標題、作者和內容類型) 。 若要取得完整記錄,用戶端會要求 /api/books/{id}/details
。
必要條件
Visual Studio 2017 Community、Professional 或 Enterprise Edition。
建立 Visual Studio 專案
從執行 Visual Studio 開始。 從 [檔案] 功能表選取 [新增],再選取 [專案]。
展開[已安裝>的 Visual C#] 類別。 在 [Visual C#] 底下,選取 [Web]。 在專案範本清單中,選取ASP.NET Web 應用程式 (.NET Framework) 。 將專案命名為 「BooksAPI」。
在 [ 新增 ASP.NET Web 應用程式 ] 對話方塊中,選取 [空白 ] 範本。 在 [新增資料夾和核心參考] 底下,選取 [Web API ] 核取方塊。 按一下 [確定]。
這會建立針對 Web API 功能設定的基本架構專案。
領域模型
接下來,新增領域模型的類別。 在 [方案總管] 中,於 Models 資料夾上按一下滑鼠右鍵。 選取 [新增],然後選取 [ 類別]。 將類別命名為 Author
。
以下列內容取代 Author.cs 中的程式碼:
using System.ComponentModel.DataAnnotations;
namespace BooksAPI.Models
{
public class Author
{
public int AuthorId { get; set; }
[Required]
public string Name { get; set; }
}
}
現在新增另一個名為 的 Book
類別。
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; }
}
}
新增 Web API 控制器
在此步驟中,我們將新增使用 Entity Framework 作為資料層的 Web API 控制器。
按 CTRL+SHIFT+B 以建置專案。 Entity Framework 會使用反映來探索模型的屬性,因此需要編譯的元件才能建立資料庫架構。
在 [方案總管] 中,於 Controllers 資料夾上按一下滑鼠右鍵。 選取 [新增],然後選取 [控制器]。
在 [ 新增 Scaffold ] 對話方塊中, 使用 Entity Framework 選取具有動作的 Web API 2 控制器。
在 [ 新增控制器] 對話方塊的 [控制器名稱] 中,輸入 「BooksController」。 選取 [使用非同步控制器動作] 核取方塊。 針對 [模型類別],選取 [書籍]。 (如果您看不到 Book
下拉式清單中所列的類別,請確定您已建置 project.) 然後按一下 [+] 按鈕。
按一下 [新增資料內容]對話方塊中的 [新增]。
按一下 [新增控制器]對話方塊中的 [新增]。 Scaffolding 會新增名為 的 BooksController
類別,以定義 API 控制器。 它也會在 Models 資料夾中新增名為 BooksAPIContext
的類別,以定義 Entity Framework 的資料內容。
植入資料庫
從 [工具] 功能表中,選取 [NuGet 套件管理員],然後選取 [ 套件管理員主控台]。
在 [Package Manager Console] 視窗中,輸入下列命令:
Add-Migration
此命令會建立 Migrations 資料夾,並新增名為 Configuration.cs 的新程式碼檔案。 開啟此檔案,並將下列程式碼新增至 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},
});
}
在 [套件管理員主控台] 視窗中,輸入下列命令。
add-migration Initial
update-database
這些命令會建立本機資料庫,並叫用 Seed 方法來填入資料庫。
新增 DTO 類別
如果您現在執行應用程式,並將 GET 要求傳送至 /api/books/1,回應看起來會像下面這樣。 (我新增了可讀性的縮排。)
{
"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
}
相反地,我想要此要求傳回欄位的子集。 此外,我想要傳回作者的名稱,而不是作者識別碼。 為了達成此目的,我們將修改控制器方法以傳回資料傳輸 物件 , (DTO) ,而不是 EF 模型。 DTO 是只設計用來傳送資料的物件。
在 [方案總管] 中,以滑鼠右鍵按一下專案,然後選取 [新增 | 資料夾]。 將資料夾命名為 「DTO」。 使用下列定義,將名為 BookDto
的類別新增至 DTO 資料夾:
namespace BooksAPI.DTOs
{
public class BookDto
{
public string Title { get; set; }
public string Author { get; set; }
public string Genre { get; set; }
}
}
新增另一個名為 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; }
}
}
接下來,更新 類別 BooksController
以傳回 BookDto
實例。 我們將使用 Queryable.Select 方法將實例投影 Book
到 BookDto
實例。 以下是控制器類別的更新程式碼。
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();
// Typed lambda expression for Select() method.
private static readonly Expression<Func<Book, BookDto>> AsBookDto =
x => new BookDto
{
Title = x.Title,
Author = x.Author.Name,
Genre = x.Genre
};
// GET api/Books
public IQueryable<BookDto> GetBooks()
{
return db.Books.Include(b => b.Author).Select(AsBookDto);
}
// GET api/Books/5
[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);
}
}
}
注意
我已刪除 PutBook
、 PostBook
和 DeleteBook
方法,因為本教學課程不需要這些方法。
現在,如果您執行應用程式並要求 /api/books/1,回應本文看起來應該如下所示:
{"Title":"Midnight Rain","Author":"Ralls, Kim","Genre":"Fantasy"}
新增路由屬性
接下來,我們會將控制器轉換成使用屬性路由。 首先,將 RoutePrefix 屬性新增至控制器。 這個屬性會定義此控制器上所有方法的初始 URI 區段。
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
// ...
然後將 [Route] 屬性新增至控制器動作,如下所示:
[Route("")]
public IQueryable<BookDto> GetBooks()
{
// ...
}
[Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
// ...
}
每個控制器方法的路由範本都是前置詞加上 Route 屬性中指定的字串。 GetBook
針對 方法,路由範本包含參數化字串 「{id:int}」,如果 URI 區段包含整數值,則會比對。
方法 | 路由範本 | 範例 URI |
---|---|---|
GetBooks |
「api/books」 | http://localhost/api/books |
GetBook |
「api/books/{id:int}」 | http://localhost/api/books/5 |
取得書籍詳細資料
若要取得書籍詳細資料,用戶端會將 GET 要求傳送至 /api/books/{id}/details
,其中 {id} 是書籍的識別碼。
將下列方法新增至 BooksController
類別。
[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);
}
如果您要求 /api/books/1/details
,回應看起來會像這樣:
{
"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 要求傳送至 /api/books/genre
,其中 內容類型是內容類型 的名稱。 (例如,/api/books/fantasy
)。
將下列方法新增至 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);
}
在這裡,我們會定義一個路由,其中包含 URI 範本中的 {genre} 參數。 請注意,Web API 能夠區分這兩個 URI,並將其路由至不同的方法:
/api/books/1
/api/books/fantasy
這是因為 GetBook
方法包含條件約束,「id」 區段必須是整數值:
[Route("{id:int}")]
public BookDto GetBook(int id)
{
// ...
}
如果您要求 /api/books/quiet,回應看起來會像這樣:
[ { "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 要求傳送至 /api/authors/id/books
,其中 id 是作者的識別碼。
將下列方法新增至 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);
}
此範例很有趣,因為「書籍」會被視為「作者」的子資源。 此模式在 RESTful API 中相當常見。
路由範本中的波浪線 (~) 會覆寫 RoutePrefix 屬性中的路由前置詞。
依發行日期取得書籍
若要依發行日期取得書籍清單,用戶端會將 GET 要求傳送至 /api/books/date/yyyy-mm-dd
,其中 yyyy-mm-dd 是日期。
以下是執行此動作的其中一種方式:
[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);
}
參數 {pubdate:datetime}
受限於符合 DateTime 值。 這可運作,但實際上比我們想要更寬鬆。 例如,這些 URI 也會符合路由:
/api/books/date/Thu, 01 May 2008
/api/books/date/2000-12-16T00:00:00
允許這些 URI 沒有任何問題。 不過,您可以將正則運算式條件約束新增至路由範本,以限制路由至特定格式的路由:
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
// ...
}
現在,只有 「yyyy-mm-dd」 格式的日期才會相符。 請注意,我們不會使用 RegEx 來驗證我們取得實際日期。 當 Web API 嘗試將 URI 區段轉換成 DateTime 實例時,就會處理此作業。 無法轉換 '2012-47-99' 之類的無效日期,且用戶端會收到 404 錯誤。
您也可以新增另一個具有不同 RegEx 的[Route]屬性,以支援斜線分隔符號 (/api/books/date/yyyy/mm/dd
) 。
[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)
{
// ...
}
這裡有一個細微但重要的詳細資料。 第二個路由範本在 {pubdate} 參數開頭有萬用字元 (*) :
{*pubdate: ... }
這會告訴路由引擎 {pubdate} 應該符合 URI 的其餘部分。 根據預設,範本參數會比對單一 URI 區段。 在此情況下,我們想要 {pubdate} 跨越數個 URI 區段:
/api/books/date/2013/06/17
控制器程式碼
以下是 BooksController 類別的完整程式碼。
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();
// Typed lambda expression for Select() method.
private static readonly Expression<Func<Book, BookDto>> AsBookDto =
x => new BookDto
{
Title = x.Title,
Author = x.Author.Name,
Genre = x.Genre
};
// GET api/Books
[Route("")]
public IQueryable<BookDto> GetBooks()
{
return db.Books.Include(b => b.Author).Select(AsBookDto);
}
// GET api/Books/5
[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);
}
}
}
總結
屬性路由可讓您在設計 API 的 URI 時有更多控制權和更大的彈性。
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應