Isaac.

Data Transfer Objects (DTOs)

Design DTOs for clean API contracts.

By EMEPublished: February 20, 2025
dtoapi designmappingcontracts

A Simple Analogy

DTOs are like translators between layers. Your internal model talks to the API model through a DTO.


Why DTOs?

  • Encapsulation: Hide internal structure
  • Versioning: Change internals without API breaks
  • Validation: Input validation at boundary
  • Projection: Expose only needed fields
  • Security: Don't expose sensitive data

Basic DTO

// Entity (internal)
public class User
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string PasswordHash { get; set; }
    public string PhoneNumber { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
}

// DTO (API contract)
public class UserDto
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string PhoneNumber { get; set; }
}

// Note: PasswordHash and IsActive are hidden

Input DTO

// Request DTO with validation
public class CreateUserRequest
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }
    
    [Required]
    [MinLength(8)]
    public string Password { get; set; }
    
    [Phone]
    public string PhoneNumber { get; set; }
}

// Handler
[HttpPost]
public async Task<ActionResult<UserDto>> CreateUser(CreateUserRequest request)
{
    var user = new User
    {
        Email = request.Email,
        PasswordHash = HashPassword(request.Password),
        PhoneNumber = request.PhoneNumber
    };
    
    await _repository.SaveAsync(user);
    return Ok(MapToDto(user));
}

Mapping with AutoMapper

// Configuration
public class UserMappingProfile : Profile
{
    public UserMappingProfile()
    {
        CreateMap<User, UserDto>();
        CreateMap<CreateUserRequest, User>()
            .ForMember(u => u.PasswordHash, 
                opt => opt.MapFrom(r => HashPassword(r.Password)));
    }
}

// Usage
public async Task<UserDto> GetUserAsync(int id)
{
    var user = await _repository.GetAsync(id);
    return _mapper.Map<UserDto>(user);
}

Nested DTOs

public class OrderDto
{
    public int Id { get; set; }
    public string OrderNumber { get; set; }
    public List<OrderItemDto> Items { get; set; }
    public CustomerDto Customer { get; set; }
}

public class OrderItemDto
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

public class CustomerDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Best Practices

  1. One per operation: Request and response DTOs separate
  2. Validation: Add data annotations
  3. Immutability: Use records when possible
  4. Versioning: Use separate DTOs for API versions
  5. Documentation: Document required/optional fields

Related Concepts

  • Value objects
  • Anemic models
  • Anti-patterns
  • API versioning

Summary

Use DTOs to create clean API contracts. Hide implementation details and provide input validation at boundaries.