创建数据传输对象 (DTO)

下载已完成项目

现在,Web API 向客户端公开数据库实体。 客户端接收直接映射到数据库表的数据。 但是,这并不总是一个好主意。 有时需要更改发送到客户端的数据的形状。 例如,您可能希望:

  • 删除循环引用 (请参阅上一部分) 。
  • 隐藏客户端不应查看的特定属性。
  • 省略一些属性以缩减有效负载大小。
  • 平展包含嵌套对象的对象图,使其更方便客户端。
  • 避免“过度发布”漏洞。 (有关 over-posting 的讨论,请参阅 模型验证 。)
  • 将服务层与数据库层分离。

为此,可以 (DTO) 定义 数据传输对象 。 DTO 是一个对象,用于定义如何通过网络发送数据。 让我们看看它如何与 Book 实体配合使用。 在 Models 文件夹中,添加两个 DTO 类:

namespace BookService.Models
{
    public class BookDto
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string AuthorName { get; set; }
    }
}

namespace BookService.Models
{
    public class BookDetailDto
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public int Year { get; set; }
        public decimal Price { get; set; }
        public string AuthorName { get; set; }
        public string Genre { get; set; }
    }
}

BookDetailDto 包括 Book 模型中的所有属性,但该 AuthorName 属性是保存作者姓名的字符串。 类 BookDto 包含 来自 BookDetailDto的属性的子集。

接下来,将 类中的 BooksController 两个 GET 方法替换为返回 DTO 的版本。 我们将使用 LINQ Select 语句从 Book 实体转换为 DTO。

// GET api/Books
public IQueryable<BookDto> GetBooks()
{
    var books = from b in db.Books
                select new BookDto()
                {
                    Id = b.Id,
                    Title = b.Title,
                    AuthorName = b.Author.Name
                };

    return books;
}

// GET api/Books/5
[ResponseType(typeof(BookDetailDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
    var book = await db.Books.Include(b => b.Author).Select(b =>
        new BookDetailDto()
        {
            Id = b.Id,
            Title = b.Title,
            Year = b.Year,
            Price = b.Price,
            AuthorName = b.Author.Name,
            Genre = b.Genre
        }).SingleOrDefaultAsync(b => b.Id == id);
    if (book == null)
    {
        return NotFound();
    }

    return Ok(book);
}

下面是由新 GetBooks 方法生成的 SQL。 可以看到 EF 将 LINQ Select 转换为 SQL SELECT 语句。

SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Title] AS [Title], 
    [Extent2].[Name] AS [Name]
    FROM  [dbo].[Books] AS [Extent1]
    INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[AuthorId] = [Extent2].[Id]

最后,修改 PostBook 方法以返回 DTO。

[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> PostBook(Book book)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    db.Books.Add(book);
    await db.SaveChangesAsync();

    // New code:
    // Load author name
    db.Entry(book).Reference(x => x.Author).Load();

    var dto = new BookDto()
    {
        Id = book.Id,
        Title = book.Title,
        AuthorName = book.Author.Name
    };

    return CreatedAtRoute("DefaultApi", new { id = book.Id }, dto);
}

注意

在本教程中,我们将通过代码手动转换为 DTO。 另一个选项是使用自动处理转换的库(例如 AutoMapper )。