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 DTOSolution with DTO
Client can set Id or CreatedDateDTO excludes those fields
API response is tightly coupled to DB schemaDTO shapes the response independently
Sensitive data (passwords, internal flags) may leakDTO only includes safe fields
Schema change breaks API contractDTO 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 BookCreateDto for POST (no Id, no CreatedDate), BookUpdateDto for PUT, and BookResponseDto for 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).

Leave a Comment