天天看点

《ASP.ENT Core 与 RESTful API 开发实战》-- (第4章)-- 读书笔记(下)

第 4 章 资源操作

4.5 创建资源

由于创建资源的 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; }
    }
}
      

在 IAuthorRepository 添加用于资源添加的方法

void AddAuthor(AuthorDto author);
      

并在 AuthorMockRepository 中实现

public void AddAuthor(AuthorDto author)
{
    author.Id = Guid.NewGuid();
    LibraryMockData.Current.Authors.Add(author);
}
      

接着在 AuthorController 中添加用于创建 Author 的 Action

[HttpPost]
public IActionResult CreateAuthor(AuthorForCreationDto authorForCreationDto)
{
    var authorDto = new AuthorDto
    {
        Name = authorForCreationDto.Name,
        Age = authorForCreationDto.Age,
        Email = authorForCreationDto.Email
    };

    AuthorRepository.AddAuthor(authorDto);

    // 返回201 Created 状态码,并在响应消息头中包含 Location 项,它的值是新创建资源的 URL
    // 第一个参数是要调用 Action 的路由名称
    // 第二个参数是包含要调用 Action 所需要参数的匿名对象
    // 最后一个参数是代表添加成功后的资源本身
    return CreatedAtRoute(nameof(GetAuthor), new {authorId = authorDto.Id}, authorDto);
}
      

由于 CreatedAtRoute 方法要生成指向 GetAuthor 方法的 URL,因此还需要为这个 Action 定义一个路由名称

[HttpGet("{authorId}", Name = nameof(GetAuthor))]
public ActionResult<AuthorDto> GetAuthor(Guid authorId)
      

创建子级资源,创建 DTO,在 IBookRepository 中接口添加方法,在 BookMockRepository 中实现类实现接口方法

namespace Library.API.Models
{
    public class BookForCreationDto
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public int Pages { get; set; }
    }
}

void AddBook(BookDto book);

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);
}
      

同样为 GetBook 方法指定路由名称

[HttpGet("{bookId}", Name = nameof(GetBook))]
public ActionResult<BookDto> GetBook(Guid authorId, Guid bookId)
      

4.6 删除资源

在 IBookRepository 中接口添加方法,在 BookMockRepository 中实现类实现接口方法

void DeleteBook(BookDto book);

public void DeleteBook(BookDto book)
{
    LibraryMockData.Current.Books.Remove(book);
}
      
[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 中接口添加方法,在 AuthorMockRepository 中实现类实现接口方法

void DeleteAuthor(AuthorDto author);

public void DeleteAuthor(AuthorDto author)
{
    LibraryMockData.Current.Books.RemoveAll(book => book.AuthorId == author.Id);
    LibraryMockData.Current.Authors.Remove(author);
}
      

4.7 更新资源

从 HTTP 方法的角度来看,更新资源有两种情况:

  • 整体更新,PUT 方法完成
  • 部分更新,PATCH 方法完成

整体更新,创建 DTO,在 IBookRepository 中接口添加方法,在 BookMockRepository 中实现类实现接口方法

namespace Library.API.Models
{
    public class BookForUpdateDto
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public int Pages { get; set; }
    }
}

void UpdateBook(Guid authorId, Guid bookId, BookForUpdateDto book);

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;
}
      
[HttpPut("{bookId}")]
public IActionResult UpdateBook(Guid authorId, Guid bookId, BookForUpdateDto updateBook)
{
    if (!AuthorRepository.IsAuthorExists(authorId))
    {
        return NotFound();
    }

    var book = BookRepository.GetBookForAuthor(authorId, bookId);
    if (book == null)
    {
        return NotFound();
    }

    BookRepository.UpdateBook(authorId, bookId, updateBook);
    return NoContent();
}
      

部分更新,PATCH 方法的请求正文使用的是 JSON Patch 文档格式

文档由一个数组构成,数组中的每个元素代表一个更改项,每一项包括3项:

  • op:操作类型
  • path:对象的属性名
  • value:对象的值

op 的值包括以下6种:

  • add
  • remove
  • replace
  • copy
  • move
  • test

因此以下内容会更新图书资源的 Title 属性,并清空 Description 属性

[
  {
    "op": "replace",
    "path": "/title",
    "value": "Book 1 - Updated"
  },
  {
    "op": "remove",
    "path": "/description" 
  }
]
      
[HttpPatch("{bookId}")]
public IActionResult PartiallyUpdateBook(Guid authorId, Guid bookId, JsonPatchDocument<BookForUpdateDto> patchDocument)
{
    if (!AuthorRepository.IsAuthorExists(authorId))
    {
        return NotFound();
    }

    var book = BookRepository.GetBookForAuthor(authorId, bookId);
    if (book == null)
    {
        return NotFound();
    }

    var bookToPatch = new BookForUpdateDto
    {
        Title = book.Title,
        Description = book.Description,
        Pages = book.Pages
    };

    patchDocument.ApplyTo(bookToPatch, ModelState);
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    BookRepository.UpdateBook(authorId, bookId, bookToPatch);
    return NoContent();
}
      

第三个参数类型为 JsonPatchDocument,它的值会从请求信息的正文中获取

ApplyTo 方法将相应的修改操作应用到新建的对象上,并将可能出现的错误记录到 ModelStateDictionary 中,使用该方法需要添加引用

dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson
      

4.8 内容协商

RESTful API 应该根据客户端的需要返回不同格式的数据

客户端指明格式是在其请求消息的消息头中添加 Accept 项,它的值是一个 MIME 类型,如 application/xml

如果支持返回此格式数据则直接返回,不支持则返回 406 NotAcceptable 状态码

以上过程称为内容协商

ASP.NET Core MVC 中,对于不支持的 Accept 类型返回 406 NotAcceptable 这一配置项默认为 false,因此它会返回默认格式,可以在 ConfigureService 方法中添加 MVC 服务时配置

services.AddMvc(configure =>
{
    configure.ReturnHttpNotAcceptable = true;
});
      

Formatter 是 ASP.NET Core 中用于处理数据输出或输入格式的组件,它分为两类:输出 Formatter 和输入 Formatter

前者满足 HTTP 请求消息头的 Accept 项,后者匹配 HTTP 请求消息头的 Content-Type 项

要使服务器能够返回 XML 格式的数据,只要将能够输出 XML 格式数据的 Formatter 添加到输出 Formatter 集合中即可

services.AddMvc(configure =>
{
    configure.ReturnHttpNotAcceptable = true;
    configure.OutputFormatters.Add(new XmlSerializerOutputFormatter());
});
      

services.AddMvc() 方法返回 IMvcBuilder 接口,可以直接调用扩展方法将 XML 格式数据输入输出都添加进来

services.AddMvc(configure =>
{
    configure.ReturnHttpNotAcceptable = true;
    //configure.OutputFormatters.Add(new XmlSerializerOutputFormatter());
}).AddXmlSerializerFormatters();
      

对于特殊格式数据,需要创建自定义 Formatter,继承自 TextOutputFormatter 类或 TextInputFormatter 类

《ASP.ENT Core 与 RESTful API 开发实战》-- (第4章)-- 读书笔记(下)