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
Never return domain entities directly from APIs. Use DTOs to control what data is exposed.
DTO Examples
Domain Model
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
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
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
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
// 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 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
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
Creating a Mapping Profile
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
builder.Services.AddAutoMapper(typeof(ProductProfile));
Using AutoMapper
[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
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
[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
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));
}
}
builder.Services.AddAutoMapper(typeof(Program));
[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
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.