Section 6 of 7

DTOs and AutoMapper

🎯 What You'll Learn

  • What DTOs are and why use them
  • Creating DTOs for different operations
  • Manual mapping vs AutoMapper
  • AutoMapper configuration
  • Mapping profiles
  • Best practices

What are DTOs?

DTOs (Data Transfer Objects) are simple objects used to transfer data between layers, separating API contracts from domain models.

Why Use DTOs?

  • Separation of concerns: API models ≠ domain models
  • Security: Don't expose internal properties
  • Flexibility: Change domain without breaking API
  • Validation: Different validation rules per operation
  • Performance: Return only needed data
⚠️ Don't Expose Domain Models

Never return domain entities directly from APIs. Use DTOs to control what data is exposed.

DTO Examples

Domain Model

Models/Product.cs C#
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public string? Description { get; set; }
    public int Quantity { get; set; }
    public string Category { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
    public bool IsDeleted { get; set; } // Internal flag
}

Read DTO

DTOs/ProductDto.cs C#
public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public string? Description { get; set; }
    public int Quantity { get; set; }
    public string Category { get; set; } = string.Empty;
    
    // No IsDeleted, CreatedAt, UpdatedAt (internal details)
}

Create DTO

DTOs/CreateProductDto.cs C#
public class CreateProductDto
{
    [Required]
    [StringLength(100, MinimumLength = 3)]
    public string Name { get; set; } = string.Empty;

    [Required]
    [Range(0.01, 1000000)]
    public decimal Price { get; set; }

    [StringLength(500)]
    public string? Description { get; set; }

    [Required]
    [Range(0, int.MaxValue)]
    public int Quantity { get; set; }

    [Required]
    public string Category { get; set; } = string.Empty;
    
    // No Id (auto-generated)
}

Update DTO

DTOs/UpdateProductDto.cs C#
public class UpdateProductDto
{
    [Required]
    [StringLength(100, MinimumLength = 3)]
    public string Name { get; set; } = string.Empty;

    [Required]
    [Range(0.01, 1000000)]
    public decimal Price { get; set; }

    [StringLength(500)]
    public string? Description { get; set; }

    [Range(0, int.MaxValue)]
    public int Quantity { get; set; }
}

Manual Mapping

Manual DTO Mapping C#
// Create: DTO → Domain
[HttpPost]
public ActionResult CreateProduct(CreateProductDto dto)
{
    var product = new Product
    {
        Name = dto.Name,
        Price = dto.Price,
        Description = dto.Description,
        Quantity = dto.Quantity,
        Category = dto.Category,
        CreatedAt = DateTime.UtcNow
    };
    
    // Save product...
    return Ok(product);
}

// Read: Domain → DTO
[HttpGet("{id}")]
public ActionResult GetProduct(int id)
{
    var product = /* get from database */;
    
    var dto = new ProductDto
    {
        Id = product.Id,
        Name = product.Name,
        Price = product.Price,
        Description = product.Description,
        Quantity = product.Quantity,
        Category = product.Category
    };
    
    return Ok(dto);
}
ℹ️ Manual Mapping

Manual mapping is verbose but explicit. Good for simple cases, but tedious for complex models.

AutoMapper

AutoMapper is a library that automatically maps between objects based on conventions.

Installation

Package Installation Bash
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

Creating a Mapping Profile

Mappings/ProductProfile.cs C#
public class ProductProfile : Profile
{
    public ProductProfile()
    {
        // Domain → DTO
        CreateMap<Product, ProductDto>();

        // DTO → Domain
        CreateMap<CreateProductDto, Product>()
            .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow));

        CreateMap<UpdateProductDto, Product>()
            .ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow));
    }
}

Register AutoMapper

Program.cs C#
builder.Services.AddAutoMapper(typeof(ProductProfile));

Using AutoMapper

Controllers/ProductsController.cs C#
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMapper _mapper;

    public ProductsController(IMapper mapper)
    {
        _mapper = mapper;
    }

    [HttpGet("{id}")]
    public ActionResult<ProductDto> GetProduct(int id)
    {
        var product = /* get from database */;
        
        // Map: Product → ProductDto
        var dto = _mapper.Map<ProductDto>(product);
        
        return Ok(dto);
    }

    [HttpPost]
    public ActionResult<ProductDto> CreateProduct(CreateProductDto dto)
    {
        // Map: CreateProductDto → Product
        var product = _mapper.Map<Product>(dto);
        
        // Save product...
        
        // Map: Product → ProductDto
        var resultDto = _mapper.Map<ProductDto>(product);
        
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, resultDto);
    }

    [HttpPut("{id}")]
    public ActionResult UpdateProduct(int id, UpdateProductDto dto)
    {
        var product = /* get from database */;
        
        // Map: UpdateProductDto → Product (update existing)
        _mapper.Map(dto, product);
        
        // Save product...
        
        return NoContent();
    }
}

Advanced Mapping

Custom Property Mapping

Custom Mappings C#
public class ProductProfile : Profile
{
    public ProductProfile()
    {
        CreateMap<Product, ProductDto>()
            // Map FullName from FirstName + LastName
            .ForMember(dest => dest.FullName, 
                opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
            
            // Ignore property
            .ForMember(dest => dest.InternalId, opt => opt.Ignore())
            
            // Conditional mapping
            .ForMember(dest => dest.Discount, 
                opt => opt.MapFrom(src => src.Price > 100 ? 0.1m : 0));
    }
}

Mapping Collections

Mapping Lists C#
[HttpGet]
public ActionResult<List<ProductDto>> GetProducts()
{
    var products = /* get from database */;
    
    // Map: List<Product> → List<ProductDto>
    var dtos = _mapper.Map<List<ProductDto>>(products);
    
    return Ok(dtos);
}

Complete InvenTrack Example

Mappings/ProductProfile.cs C#
public class ProductProfile : Profile
{
    public ProductProfile()
    {
        // Read: Domain → DTO
        CreateMap<Product, ProductDto>();

        // Create: DTO → Domain
        CreateMap<CreateProductDto, Product>()
            .ForMember(dest => dest.Id, opt => opt.Ignore())
            .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow))
            .ForMember(dest => dest.IsDeleted, opt => opt.MapFrom(_ => false));

        // Update: DTO → Domain
        CreateMap<UpdateProductDto, Product>()
            .ForMember(dest => dest.Id, opt => opt.Ignore())
            .ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
            .ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow));
    }
}
Program.cs C#
builder.Services.AddAutoMapper(typeof(Program));
Controllers/ProductsController.cs C#
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly IMapper _mapper;

    public ProductsController(IProductService productService, IMapper mapper)
    {
        _productService = productService;
        _mapper = mapper;
    }

    [HttpGet]
    public async Task<ActionResult<List<ProductDto>>> GetProducts()
    {
        var products = await _productService.GetAllAsync();
        var dtos = _mapper.Map<List<ProductDto>>(products);
        return Ok(dtos);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<ProductDto>> GetProduct(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        
        if (product == null)
            return NotFound();
        
        var dto = _mapper.Map<ProductDto>(product);
        return Ok(dto);
    }

    [HttpPost]
    public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductDto dto)
    {
        var product = _mapper.Map<Product>(dto);
        var created = await _productService.CreateAsync(product);
        var resultDto = _mapper.Map<ProductDto>(created);
        
        return CreatedAtAction(nameof(GetProduct), new { id = created.Id }, resultDto);
    }

    [HttpPut("{id}")]
    public async Task<ActionResult> UpdateProduct(int id, UpdateProductDto dto)
    {
        var product = await _productService.GetByIdAsync(id);
        
        if (product == null)
            return NotFound();
        
        _mapper.Map(dto, product);
        await _productService.UpdateAsync(product);
        
        return NoContent();
    }
}

Best Practices

  • Always use DTOs: Never expose domain models
  • Separate DTOs: Different DTOs for Create/Update/Read
  • AutoMapper profiles: Organize mappings by feature
  • Explicit mapping: Use ForMember for clarity
  • Validation: Add validation attributes to DTOs
  • Naming: Use consistent DTO naming (CreateXDto, UpdateXDto, XDto)
  • Security: Don't include sensitive fields in DTOs

Key Takeaways

  • DTOs: Separate API contracts from domain models
  • Security: Control what data is exposed
  • Flexibility: Change domain without breaking API
  • AutoMapper: Automate object mapping
  • Profiles: Organize mapping configuration
  • ForMember: Custom property mapping
  • Collections: Map lists automatically
  • Validation: Different rules per DTO
🎯 Next Steps

You now understand DTOs and AutoMapper! In the final section, we'll explore API Versioning—how to evolve your API over time while maintaining backward compatibility.