Web API 2 支持一种新型的路由,称为属性路由。属性路由的一般概述,请参阅属性路由 Web API 2 中。在本教程中,您将使用属性路由创建一个 REST API 集合的书。API 将支持以下操作 ︰
行动 | URI 的示例 |
---|---|
得到的所有书的列表。 | / api/书 |
得到一本书的 id。 | /api/books/1 |
获得一本书的详细信息。 | /api/books/1/details |
按流派获得书籍的列表。 | /api/books/fantasy |
按出版日期获取书籍的列表。 | /api/books/date/2013-02-16 /api/books/date/2013/02/16 (备用窗体) |
获取一个特定作者的书籍列表。 | /api/authors/1/books |
所有方法都是只读的 (HTTP GET 请求)。
对于数据层,我们将使用实体框架。本书记录将有以下字段 ︰
- ID
- 标题
- 体裁
- 出版日期
- 价格
- 描述
- 作者 Id (Authors 表的外键)
然而,对于大多数请求,API 将返回此数据 (标题、 作者和流派) 的一个子集。要获得完整的记录,客户端请求/api/books/{id}/details
.
系统必备组件
创建 Visual Studio 项目
首先运行 Visual Studio。从文件菜单中,选择新建,然后选择项目.
在模板窗格中,选择已安装的模板和展开Visual C#节点。在Visual C#,选择Web。在项目模板的列表中,选择ASP.NET MVC 4 Web 应用程序。"BooksAPI"为项目命名。
在新的 ASP.NET 项目对话框中,选择的空模板。在"添加文件夹和核心的参考文件"下选择Web API复选框。单击创建项目.
这将创建一个为 Web API 功能配置的骨架项目。
域模型
接下来,添加域模型的类。在解决方案资源管理器中,右键单击模型文件夹。选择添加,然后选择类别。名称类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 控制器
在此步骤中,我们将添加一个 Web API 控制器,使用实体框架和数据层。
按 CTRL + SHIFT + B 来生成项目。实体框架使用反射来发现性能的模型,所以它需要在编译的程序集来创建数据库模式。
在解决方案资源管理器中,右键单击控制器文件夹。选择添加,然后选择控制器.
在添加脚手架对话框中,选择"Web API 2 控制器读取/写入操作,使用实体框架。"
在添加控制器对话框中,为控制器的名称,输入"例子"。选中"使用异步控制器操作"复选框。对于模型类,请选择"书"。(如果你看不到下拉列表中列出的Book
类,请确保生成项目时。然后单击"< 新数据上下文...>"按钮。
在新的数据上下文的对话框中,单击添加。
在添加控制器对话框中,单击添加。脚手架将添加一个名为BooksController
定义 API 控制器类。它还添加了一个名为BooksAPIContext
模型文件夹,为实体框架定义的数据上下文类。
种子的数据库
从工具菜单中,选择库软件包管理器,然后选择程序包管理器控制台.
在程序包管理器控制台窗口中,输入以下命令 ︰
enable-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
这些命令创建一个本地数据库,并调用该种子方法来填充该数据库。
添加 DTO 类
如果您运行该应用程序现在并向 /api/books/1 发送一个 GET 请求,响应看起来类似于以下内容。(我添加可读性的缩进)。
{
"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
}
相反,我希望这一请求返回的字段的子集。此外,我想它返回作者的名字,而不是作者 id。为了实现这个目标,我们将修改控制器方法返回数据传输对象(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("")]
public IQueryable<BookDto> GetBooks()
{
// ...
} [Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
// ...
}
每个控制器方法的工艺路线模板是前缀加上路由属性中指定的字符串。对于GetBook
方法,路线模板包含的参数化的字符串"{id: int}",如果 URI 段包含一个 integet 值匹配。
方法 | 工艺路线模板 | URI 的示例 |
---|---|---|
GetBooks |
"api/书" | http://localhost/api/books |
GetBook |
"api/书 / {id: int}" | http://localhost/api/books/5 |
获取书详细信息
要获得书的详细信息,客户端将发送一个 GET 请求到/api/books/{id}/details
,其中{id}是这本书的 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.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);
}
如果您请求/api/books/1/detail
,反应看起来像这样 ︰
{
"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
,其中体裁是流派的名称。(例如, /get/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 模板中的 {体裁} 参数。请注意,Web API 能够区分这两个 Uri 并将它们路由到不同的方法 ︰
/api/books/1
/api/books/fantasy
这是因为GetBook
方法包括"id"部分必须是整数值的约束 ︰
[Route("{id:int}")]
public BookDto GetBook(int id)
{
// ...
}
如果您请求 /api/books/fantasy,反应看起来像这样 ︰
[ { "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在哪里作者的 ID。
将以下方法添加到BooksController
.
[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);
}
这个例子是有趣的因为"书"治疗"作者"子资源。这种模式是很常见的 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}
参数约束相匹配的日期时间值。这工作,但它是比我们想的其实更加宽容。例如,这些 Uri 也将匹配的路由 ︰
/api/books/date/Thu, 01 May 2008
/api/books/date/2000-12-16T00:00:00
没什么毛病允许这些 Uri。但是,可以通过将正则表达式约束添加到工艺路线模板,路线限制对特定的格式 ︰
[Route("api/books/date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
// ...
}
现在只在窗体中的日期,"yyyymmdd"将相匹配。请注意,我们不使用正则表达式来验证我们得到了真正的约会。这被处理时 Web API 试图将 URI 段转换成一个DateTime实例。无效的日期如 ' 2012年-47-99' 将无法转换,并且客户端将会得到一个 404 错误。
您也可以通过添加另一个[路径]属性与一个不同的正则表达式支持斜杠分隔符 (/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: ... }
这就告诉路由引擎那 {强加} 应配合其余的 URI。默认情况下,模板参数匹配一个单一的 URI 部分。在这种情况下,我们想 {强加} 要跨几个 URI 片段 ︰
/api/books/date/2013/06/17
控制器代码
这里是像下面这样类的完整代码。
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")]publicIQueryable<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})}")]publicIQueryable<BookDto>GetBooks(DateTime pubdate){return db.Books.Include(b => b.Author).Where(b =>DbFunctions.TruncateTime(b.PublishDate)==DbFunctions.TruncateTime(pubdate)).Select(AsBookDto);}protectedoverridevoidDispose(bool disposing){
db.Dispose();base.Dispose(disposing);}}}
摘要
属性路由给你更多的控制和更大的灵活性时设计您的 API 的 Uri。
这篇文章的初衷是在 2013 年 6 月 26 日