Create Data Transfer Objects (DTOs)
by Mike Wasson
Right now, our web API exposes the database entities to the client. The client receives data that maps directly to your database tables. However, that’s not always a good idea. Sometimes you want to change the shape of the data that you send to client. For example, you might want to:
- Remove circular references (see previous section).
- Hide particular properties that clients are not supposed to view.
- Omit some properties in order to reduce payload size.
- Flatten object graphs that contain nested objects, to make them more convenient for clients.
- Avoid “over-posting” vulnerabilities. (See Model Validation for a discussion of over-posting.)
- Decouple your service layer from your database layer.
To accomplish this, you can define a data transfer object (DTO). A DTO is an object that defines how the data will be sent over the network. Let’s see how that works with the Book entity. In the Models folder, add two DTO classes:
[!code-csharpMain]
1: namespace BookService.Models
2: {
3: public class BookDTO
4: {
5: public int Id { get; set; }
6: public string Title { get; set; }
7: public string AuthorName { get; set; }
8: }
9: }
10:
11: namespace BookService.Models
12: {
13: public class BookDetailDTO
14: {
15: public int Id { get; set; }
16: public string Title { get; set; }
17: public int Year { get; set; }
18: public decimal Price { get; set; }
19: public string AuthorName { get; set; }
20: public string Genre { get; set; }
21: }
22: }
The BookDetailDTO
class includes all of the properties from the Book model, except that AuthorName
is a string that will hold the author name. The BookDTO
class contains a subset of properties from BookDetailDTO
.
Next, replace the two GET methods in the BooksController
class, with versions that return DTOs. We’ll use the LINQ Select statement to convert from Book entities into DTOs.
[!code-csharpMain]
1: // GET api/Books
2: public IQueryable<BookDTO> GetBooks()
3: {
4: var books = from b in db.Books
5: select new BookDTO()
6: {
7: Id = b.Id,
8: Title = b.Title,
9: AuthorName = b.Author.Name
10: };
11:
12: return books;
13: }
14:
15: // GET api/Books/5
16: [ResponseType(typeof(BookDetailDTO))]
17: public async Task<IHttpActionResult> GetBook(int id)
18: {
19: var book = await db.Books.Include(b => b.Author).Select(b =>
20: new BookDetailDTO()
21: {
22: Id = b.Id,
23: Title = b.Title,
24: Year = b.Year,
25: Price = b.Price,
26: AuthorName = b.Author.Name,
27: Genre = b.Genre
28: }).SingleOrDefaultAsync(b => b.Id == id);
29: if (book == null)
30: {
31: return NotFound();
32: }
33:
34: return Ok(book);
35: }
Here is the SQL generated by the new GetBooks
method. You can see that EF translates the LINQ Select into a SQL SELECT statement.
[!code-sqlMain]
1: SELECT
2: [Extent1].[Id] AS [Id],
3: [Extent1].[Title] AS [Title],
4: [Extent2].[Name] AS [Name]
5: FROM [dbo].[Books] AS [Extent1]
6: INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[AuthorId] = [Extent2].[Id]
Finally, modify the PostBook
method to return a DTO.
[!code-csharpMain]
1: [ResponseType(typeof(Book))]
2: public async Task<IHttpActionResult> PostBook(Book book)
3: {
4: if (!ModelState.IsValid)
5: {
6: return BadRequest(ModelState);
7: }
8:
9: db.Books.Add(book);
10: await db.SaveChangesAsync();
11:
12: // New code:
13: // Load author name
14: db.Entry(book).Reference(x => x.Author).Load();
15:
16: var dto = new BookDTO()
17: {
18: Id = book.Id,
19: Title = book.Title,
20: AuthorName = book.Author.Name
21: };
22:
23: return CreatedAtRoute("DefaultApi", new { id = book.Id }, dto);
24: }
[!NOTE] In this tutorial, we’re converting to DTOs manually in code. Another option is to use a library like AutoMapper that handles the conversion automatically.
|