123
对于REST而言,作者和图书都是资源,而在线图书馆项目通过RESTful API向外提供了对这些资源的添加,删除,查询和修改等操作。
本章中将使用数据传输对象(Data Transfer Object,DTO)来表示作者及图书两种不同的资源。由于DTO会返回给请求API的客户端,因此它决定了资源的表现形式。
在下一章中,我们将使用Entity Framework Core 及实体类,在这种情况下查询数据时,实体类应首先转换为DTO,而在添加,更新数据之前,则应将DTO转换为实体类。
1.创建项目 Library.API。
添加文件Modles→创建2个类 AuthorDto和BookDto
namespace Library.API.Models
{
public class AuthorDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
}
namespace Library.API.Models
{
public class BookDto
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public int Pages { get; set; }
public Guid AuthorId { get; set; }
}
}
2.使用内存数据
创建内存数据源
(在创建和测试Web API项目时,为应用程序提供一些测试数据或者模拟数据是非常有用的。尤其是在真实业务数据还不存在的情况下。要生成模拟数据,最常见的方法是使用内存数据,即在应用程序中定义一个类,专门用于提供模拟数据。另外,这些数据在应用程序每次启动时都会重新创建,为测试API保证了数据的一致性)
创建文件夹Data→添加类LibraryMockData
namespace Library.API.Data
{
public class LibraryMockData
{
**public static LibraryMockData Current { get; } = new LibraryMockData();//静态属性Current获取LibratyMockData实例**
public List<AuthorDto> Authors { get; set; }
public List<BookDto> Books { get; set; }
public LibraryMockData()
{
Authors = new List<AuthorDto>
{
new AuthorDto{ Id=new Guid("72D5B5F5"),Name = "Author 1",Age=46,Email="authoor1@xxx.com"},
new AuthorDto{ Id=new Guid("72D5B48E"),Name = "Author 1",Age=46,Email="authoor2@xxx.com"}
};
Books = new List<BookDto>
{
new BookDto { Id = new Guid("7Do4A48E"), Title = "Book1", Description = "Description of Book 1", Pages = 440, AuthorId = new Guid("80be") },
new BookDto { Id = new Guid("7Do4A48B"), Title = "Book2", Description = "Description of Book 1", Pages = 440, AuthorId = new Guid("81be") },
new BookDto { Id = new Guid("7Do4A48V"), Title = "Book3", Description = "Description of Book 1", Pages = 440, AuthorId = new Guid("82be") },
new BookDto { Id = new Guid("7Do4A48H"), Title = "Book4", Description = "Description of Book 1", Pages = 440, AuthorId = new Guid("83be") }
};
}
}
}
3.仓储模式
作为领域驱动设计(DDD)的一部分,主要用于解耦。使业务逻辑层在存储,访问数据库时无须关心数据的来源及存储方式。例如哪种类型的数据库(可能是XML),也无须关心对数据的操作,如数据库连接和命令等。所有这些直接对数据的操作均封装在具体的仓储实现中。
实现仓储模式的方式有多种,其中一种简单的方法是对每一个与数据库交互的业务对象创建一个仓储接口及其实现。好处是对一种数据对象可以跟欢迎其实际情况来定义接口的成员。比如有些对象只需要读,那么在其仓储接口中就不需要定义Update,Insert等成员。另外一种就是创建一个通用仓储接口,然后所有其他仓储接口都继承这个接口。第5章讲。
实现仓储模式
新建文件夹Services,添加接口及其具体仓储实现。
namespace Library.API.Services
{
public interface IAuthorRepository
{
IEnumerable<AuthorDto> GetAuthors();
AuthorDto GetAuthor(Guid authorId);
bool IsAuthorExists(Guid authorId);
void AddAuthor(AuthorDto author);
void DeleteAuthor(AuthorDto author);
}
}
namespace Library.API.Services
{
public interface IBookRepository
{
IEnumerable<BookDto> GetBooksForAuthor(Guid auhorId);
BookDto GetBookForAuthor(Guid authorId, Guid bookId);
void AddBook(BookDto book);
void DeleteBook(BookDto book);
void UpdateBook(Guid authorId, Guid bookId, BookForUpdateDto book);
}
}
namespace Library.API.Services
{
public class AuthorMockRepository : IAuthorRepository
{
public void AddAuthor(AuthorDto author)
{
author.Id = Guid.NewGuid();
LibraryMockData.Current.Authors.Add(author);
}
public void DeleteAuthor(AuthorDto author)
{
LibraryMockData.Current.Books.RemoveAll(book => book.AuthorId == author.Id);
LibraryMockData.Current.Authors.Remove(author);
}
public AuthorDto GetAuthor(Guid authorId)
{
var author = LibraryMockData.Current.Authors.FirstOrDefault(au => au.Id == authorId);
return author;
}
public IEnumerable<AuthorDto> GetAuthors()
{
return LibraryMockData.Current.Authors;
}
public bool IsAuthorExists(Guid authorId)
{
return LibraryMockData.Current.Authors.Any(au => au.Id == authorId);
}
}
}
namespace Library.API.Services
{
public class BookMockRepository : IBookRepository
{
public void AddBook(BookDto book)
{
LibraryMockData.Current.Books.Add(book);
}
public void DeleteBook(BookDto book)
{
LibraryMockData.Current.Books.Remove(book);
}
public BookDto GetBookForAuthor(Guid authorId, Guid bookId)
{
return LibraryMockData.Current.Books.FirstOrDefault(b => b.AuthorId == authorId && b.Id == bookId);
}
public IEnumerable<BookDto> GetBooksForAuthor(Guid auhorId)
{
return LibraryMockData.Current.Books.Where(b => b.AuthorId == auhorId).ToList();
}
public void UpdateBook(Guid authorId, Guid bookId, BookForUpdateDto book)
{
var originalBook = GetBookForAuthor(authorId, bookId);
originalBook.Title = book.Title;
originalBook.Pages = book.Pages;
originalBook.Description = book.Description;
}
}
}
4.StartUp类
services.AddScoped<IAuthorRepository, AuthorMockRepository>();
services.AddScoped<IBookRepository, BookMockRepository>();
5.为不同的资源创建其对应的Controller,及Action来实现数据访问。
(由于Web API作为向外公开的接口,其路由名称应固定。为了实现对数据的操作,如获取,创建和更新等,接下来就需要在AuthorController中添加相应的Action,并将[HttpGet][HttpPost][HttpPut]等特性应用到这些Action上。)
添加API控制器
获取资源
添加API控制器 AuthorController
public class AuthorController : ControllerBase
{
public IAuthorRepository AuthorRepository { get; }
public AuthorController(IAuthorRepository authorRepository)
{
AuthorRepository = authorRepository;
}
[HttpGet]
public ActionResult<List<AuthorDto>> GetAuthors()//获取所有作者的信息
{
return AuthorRepository.GetAuthors().ToList();
}
[HttpGet("{authorId}")]
public ActionResult<AuthorDto> GetAuthor(Guid authorId)
{
var author = AuthorRepository.GetAuthor(authorId);
if(author == null)
{
return NotFound();
}
else
{
return author;
}
}
→获取集合(是最简单的接口)如获取所有作者的信息
—/api/authors
→获取单个资源(就需要在URL中指定资源的 唯一标识)
—/api/authors/{authorId}
添加API控制器 BookController
public class BookController : ControllerBase
{
public IAuthorRepository AuthorRepository { get; }
public IBookRepository BookRepository { get; }
public BookController(IBookRepository bookRepositiry,IAuthorRepository authorRepository)
{
AuthorRepository = authorRepository;
BookRepository = bookRepositiry;
}
[HttpGet]
public ActionResult<List<BookDto>> GetBooks(Guid authorId)
{
if(!AuthorRepository.IsAuthorExists(authorId))
{
return NotFound();
}
return BookRepository.GetBooksForAuthor(authorId).ToList();
}
[HttpGet("{bookId}")]
public ActionResult<BookDto> GetBook(Guid authorId,Guid bookId)
{
if(!AuthorRepository.IsAuthorExists(authorId))
{
return NotFound();
}
var targetBook = BookRepository.GetBookForAuthor(authorId, bookId);
if(targetBook == null)
{
return NotFound();
}
return targetBook;
}
→获取父/子形式的资源(前面的例子是父级资源,接下来获取子级资源)如获取属于某个作者的所有图书信息以及其中某一本书的信息
-----/api/authors/{authorId}/books
创建资源
若要创建资源,需要使用POST方法。
POST方法可以从HTTP请求消息中的正文获取请求方所提交的数据,然后通过模型绑定传递给Action中的参数、而Action则负责调用仓储接口来完成资源的创建。
注意的是,当客户端发起POST请求时,在其请求消息的正文中包含待创建的资源,然而资源在创建之前,其Id属性还没有值,这个Id属性会在服务器生成。因此在创建资源时,不建议使用与获取数据时相同的DTO,而要单独创建一个新的DTO类
namespace Library.API.Models
{
public class AuthorForCreationDto
{
[Required(ErrorMessage = "必须提供姓名")]
[MaxLength(20,ErrorMessage ="姓名的最大长度为20个字符")]
public string Name { get; set; }
public int Age { get; set; }
[EmailAddress(ErrorMessage ="邮箱格式不正确")]
public string Email { get; set; }
}
}
创建以上的DTO类以后,同时也要在IAuthorRepository接口中添加资源的方法
void AddAuthor(AduthorDto author)
并在具体类AuthorRepository实现
public void AddAuthor(AuthorDto author)
{
author.Id = Guid.NewGuid();
LibraryMockData.Current.Authors.Add(author);
}
最后在AuthorController中添加用于创建Author的Action。
要创建AuthorDto,首先要将AuthorForCreationDto转换为AuthorDto,并调用IAuthorRepository接口的AddAuthor方法添加。
当添加成功后,需要调用ControllerBase类的CreatedAtRoute方法返回 ,它的值是新创建资源的URL。
由于CreatedAtRoute方法要生成指向GetAuthor方法的URL,因此还需要为这个Action定义一个路由名称。
[HttpGet("{authorId}",Name=nameof(GetAuthor))]
public ActionResult<AuthorDto> GetAuthor(Guid authorId)
{
}
//第一个参数是要调用Action的路由名称,第二个参数是包含要调用Action所需要参数的匿名对象,最后一个参数是代码添加成功后的资源本身。
public virtual CreateAtRouteResult CreatedAtRoute(string routeName,object routeValus,object value);
[HttpPost]
public IActionResult CreateAuthor(AuthorForCreationDto authorForCreationDto)
{
var authorDto = new AuthorDto { Name = authorForCreationDto.Name,Age = authorForCreationDto.Age,Email=authorForCreationDto.Email};
AuthorRepository.AddAuthor(authorDto);
return CreatedAtRoute(nameof(GetAuthor), new { authorId = authorDto.Id }, authorDto);
}
创建子级资源
与创建父级资源一样,创建子级资源同样也要新建一个单独的DTO类,用于描述要创建的资源。
public class BookForCreationDto
{
public string Title { get; set; }
public string Description { get; set; }
public int Pages { get; set; }
}
在接口IBookRepository添加以下成员,用于创建Book资源
void AddBook(BookDto book);
及BookRepository实现方法
public void AddBook(BookDto book)
{
LibraryMockData.Current.Books.Add(book);
}
最后在BookController中添加一个Action
[HttpPost]
public IActionResult AddBook(Guid authorId,BookForCreationDto bookForCreationDto)
{
if(!AuthorRepository.IsAuthorExists(authorId))
{ return NotFound(); }
var newBook = new BookDto
{
Id = Guid.NewGuid(),
Title = bookForCreationDto.Title,
Description = bookForCreationDto.Description,
Pages = bookForCreationDto.Pages,
AuthorId = authorId,
};
BookRepository.AddBook(newBook);
return CreatedAtRoute(nameof(GetBook), new { authorId = authorId, bookId = newBook.Id }, newBook);
}
AddBook方法支持两个参数,第一个参数authorId的值从为BookController定义的路由中获取,而第二个参数的值则从请求消息的正文中解析到。
在创建子级资源时,首先要判断父级资源是否存在。如果存在,则进行类型转换。将BookForCreationDto转换为BookDto,并调用仓储接口的方法创建。
删除资源
IBookRepository 添加DeleteBook方法
void DeleteBook(BookDto book);
BookMockRepository实现方法
public void DeleteBook(BookDto book)
{
LibratyMockData.Current.Books.Remove(book);
}
控制器里BookController中添加一个新Action,并为其添加[HttpDelete("{bookID}")]
[HttpDelete("{bookID}")]
public IActionResult DeleteBook(Guid authorId,Guid bookId)
{
if(!AuthorRepository.IsAuthorExists(authorId))
{
return NotFound();
}
var book = BookRepository.GetBookForAuthor(authorId, bookId);
if(book == null)
{
return NotFound();
}
BookRepository.DeleteBook(book);
return NoContent();
}
删除父与子
当删除一个父级资源时,其所有相关的子级资源也应一同删除。因此在删除作者时,与它相关的图书资源也都要一同删除。
IAuthorRepository接口添加新成员
void DeleteAuthor(AuthorDto author);
AuthorRepository添加其相应的实现方法。
public void DeleteAuthor(AuthorDto author)
{
LibraryMockData.Current.Books.RemoveAll(book => book.AuthorId == author.Id);
LibraryMockData.Current.Authors.Remove(author);
}
控制器里添加删除的Action
[HttpDelete("{bookID}")]
public IActionResult DeleteBook(Guid authorId,Guid bookId)
{
if(!AuthorRepository.IsAuthorExists(authorId))
{
return NotFound();
}
var book = BookRepository.GetBookForAuthor(authorId, bookId);
if(book == null)
{
return NotFound();
}
BookRepository.DeleteBook(book);
return NoContent();
}
更新资源
整体更新资源→有PUT方法
部分更新资源→有PATCH方法
IBookRepository接口
void UpdateBook(Guid authorId, Guid bookId, BookForUpdateDto book);
BookRepository实现
public void UpdateBook(Guid authorId, Guid bookId, BookForUpdateDto book)
{
var originalBook = GetBookForAuthor(authorId, bookId);
originalBook.Title = book.Title;
originalBook.Pages = book.Pages;
originalBook.Description = book.Description;
}
在Models文件夹中创建一个类BookForUpdateDto.这个类会作为Action的参数,也就是说,请求消息的正文会解析为该类型。它与BookDto类相比,没有Id和AuthorId属性,这是因为这两个属性值在URL中会指定,所以在这里不再需要。
控制器里添加一个Action
[HttpDelete("{bookID}")]
public IActionResult DeleteBook(Guid authorId,Guid bookId)
{
if(!AuthorRepository.IsAuthorExists(authorId))
{
return NotFound();
}
var book = BookRepository.GetBookForAuthor(authorId, bookId);
if(book == null)
{
return NotFound();
}
BookRepository.DeleteBook(book);
return NoContent();
}
UpdateBook方法包括3个参数,其中前2个参数已在路由模板中指定,它们的值从URL中获取。而第三个参数则应在请求消息的正文中向其提供。
实现内容协商 略