建立資料傳輸物件 (DTO)

下載已完成的專案

現在,我們的 Web API 會將資料庫實體公開給用戶端。 用戶端會接收直接對應至資料庫資料表的資料。 不過,這不一定是個不錯的主意。 有時候您想要變更傳送至用戶端的資料圖形。 例如,您可能要:

  • (移除迴圈參考,請參閱上一節) 。
  • 隱藏用戶端不應該檢視的特定屬性。
  • 省略某些屬性,以減少承載大小。
  • 包含巢狀物件的扁平化物件圖形,使其更方便用戶端使用。
  • 避免「過度張貼」弱點。 (請參閱 模型驗證 ,以取得過度張貼的討論。)
  • 將服務層與資料庫層分離。

若要達成此目的,您可以在 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