Core API DTOs and AutoMapper
The BookStore API currently exposes the Book model directly to clients. This is a problem — the model is tied to the database schema. If the schema changes, the API response changes too. Also, some fields (like CreatedDate) should not be sent by clients, but there is nothing stopping them from doing so. DTOs (Data Transfer Objects) and AutoMapper solve this.
What Is a DTO?
A DTO is a simple class designed specifically for transferring data between the client and the API. It contains only the fields that are relevant for that particular operation — no database-specific fields, no internal IDs in creation requests.
Database Model (Book): DTO for creating (BookCreateDto):
───────────────────────────── ─────────────────────────────────
Id (set by DB)
Title ────────────────▶ Title (client provides this)
Author ────────────────▶ Author (client provides this)
Price ────────────────▶ Price (client provides this)
Category ────────────────▶ Category (client provides this)
IsAvailable ────────────────▶ IsAvailable(client provides this)
CreatedDate (set by server) ← NOT in DTO
Why Use DTOs?
| Problem Without DTO | Solution with DTO |
|---|---|
| Client can set Id or CreatedDate | DTO excludes those fields |
| API response is tightly coupled to DB schema | DTO shapes the response independently |
| Sensitive data (passwords, internal flags) may leak | DTO only includes safe fields |
| Schema change breaks API contract | DTO acts as a stable contract layer |
Creating DTOs for the BookStore API
Create a DTOs folder and add three DTO files:
BookCreateDto – For Creating a Book (POST)
// DTOs/BookCreateDto.cs
using System.ComponentModel.DataAnnotations;
namespace BookStoreAPI.DTOs
{
public class BookCreateDto
{
[Required(ErrorMessage = "Title is required.")]
[StringLength(200, MinimumLength = 2)]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "Author is required.")]
[StringLength(100, MinimumLength = 2)]
public string Author { get; set; } = string.Empty;
[Required]
[Range(0.01, 9999.99)]
public decimal Price { get; set; }
[Required]
public string Category { get; set; } = string.Empty;
public bool IsAvailable { get; set; } = true;
}
}
BookUpdateDto – For Updating a Book (PUT)
// DTOs/BookUpdateDto.cs
using System.ComponentModel.DataAnnotations;
namespace BookStoreAPI.DTOs
{
public class BookUpdateDto
{
[Required]
[StringLength(200, MinimumLength = 2)]
public string Title { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 2)]
public string Author { get; set; } = string.Empty;
[Required]
[Range(0.01, 9999.99)]
public decimal Price { get; set; }
[Required]
public string Category { get; set; } = string.Empty;
public bool IsAvailable { get; set; }
}
}
BookResponseDto – For Returning a Book (GET)
// DTOs/BookResponseDto.cs
namespace BookStoreAPI.DTOs
{
public class BookResponseDto
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
public bool IsAvailable { get; set; }
public DateTime CreatedDate { get; set; }
}
}
What Is AutoMapper?
Manually mapping a Book to a BookResponseDto means writing: dto.Title = book.Title; dto.Author = book.Author; ... for every field. AutoMapper eliminates this repetitive code by automatically mapping properties with the same name.
Installing AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
Creating a Mapping Profile
// Mappings/BookMappingProfile.cs
using AutoMapper;
using BookStoreAPI.DTOs;
using BookStoreAPI.Models;
namespace BookStoreAPI.Mappings
{
public class BookMappingProfile : Profile
{
public BookMappingProfile()
{
// Book → BookResponseDto (for GET responses)
CreateMap<Book, BookResponseDto>();
// BookCreateDto → Book (for POST requests)
CreateMap<BookCreateDto, Book>();
// BookUpdateDto → Book (for PUT requests)
CreateMap<BookUpdateDto, Book>();
}
}
}
Registering AutoMapper in Program.cs
// Program.cs
builder.Services.AddAutoMapper(typeof(Program));
Updating the Controller to Use DTOs
// Controllers/BooksController.cs
using AutoMapper;
using BookStoreAPI.DTOs;
using BookStoreAPI.Models;
using BookStoreAPI.Services;
using Microsoft.AspNetCore.Mvc;
namespace BookStoreAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly IBookService _bookService;
private readonly IMapper _mapper;
public BooksController(IBookService bookService, IMapper mapper)
{
_bookService = bookService;
_mapper = mapper;
}
// GET /api/books
[HttpGet]
public async Task<IActionResult> GetAll()
{
var books = await _bookService.GetAllAsync();
var result = _mapper.Map<List<BookResponseDto>>(books);
return Ok(result);
}
// GET /api/books/1
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
var book = await _bookService.GetByIdAsync(id);
if (book == null) return NotFound();
var result = _mapper.Map<BookResponseDto>(book);
return Ok(result);
}
// POST /api/books
[HttpPost]
public async Task<IActionResult> Create([FromBody] BookCreateDto dto)
{
var book = _mapper.Map<Book>(dto);
var created = await _bookService.CreateAsync(book);
var result = _mapper.Map<BookResponseDto>(created);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
// PUT /api/books/1
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, [FromBody] BookUpdateDto dto)
{
var updatedBook = _mapper.Map<Book>(dto);
var success = await _bookService.UpdateAsync(id, updatedBook);
if (!success) return NotFound();
return NoContent();
}
// DELETE /api/books/1
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
var success = await _bookService.DeleteAsync(id);
if (!success) return NotFound();
return NoContent();
}
}
}
How AutoMapper Works
Book (from database):
Id = 1
Title = "Clean Code"
Author = "Robert C. Martin"
Price = 29.99
Category = "Technology"
IsAvailable = true
CreatedDate = 2024-01-01
_mapper.Map<BookResponseDto>(book) →
BookResponseDto:
Id = 1 ← matched by name
Title = "Clean Code" ← matched by name
Author = "Robert C. Martin"← matched by name
Price = 29.99 ← matched by name
Category = "Technology" ← matched by name
IsAvailable = true ← matched by name
CreatedDate = 2024-01-01 ← matched by name
Updated Project Structure
BookStoreAPI/
├── Controllers/
│ └── BooksController.cs ← Updated (uses DTOs + IMapper)
├── DTOs/ ← NEW
│ ├── BookCreateDto.cs ← NEW
│ ├── BookUpdateDto.cs ← NEW
│ └── BookResponseDto.cs ← NEW
├── Mappings/ ← NEW
│ └── BookMappingProfile.cs ← NEW
├── Models/
│ └── Book.cs
├── Repositories/
├── Services/
└── Program.cs ← Updated (AutoMapper registered)
Key Points
- DTOs separate the API contract from the database model — they control exactly what the client sends and receives.
- Use
BookCreateDtofor POST (no Id, no CreatedDate),BookUpdateDtofor PUT, andBookResponseDtofor GET responses. - AutoMapper automatically maps properties with matching names between two classes.
- A mapping profile defines which types map to which — registered once in
Program.cs. - The controller maps DTOs to models (for input) and models to DTOs (for output) using
_mapper.Map<T>(source).
