1. Serialização — de objeto C# para JSON, e vice-versa
O ASP.NET Core usa System.Text.Json por padrão para converter objetos
C# em JSON (na resposta) e JSON em objetos C# (no corpo de um POST/PUT) — automaticamente, sem
você escrever esse código.
public class ClienteDto
{
public int Id { get; set; }
public string Nome { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
// return Ok(new ClienteDto { Id = 1, Nome = "Ana", Email = "ana@x.com" });
// gera:
// { "id": 1, "nome": "Ana", "email": "ana@x.com" }
Por padrão, o ASP.NET Core converte nomes de propriedade PascalCase (convenção C#, capítulo 14
do Módulo 1) para camelCase no JSON (convenção JavaScript) — Nome
vira nome. Isso já resolve a maior fricção de convenção entre as
duas linguagens automaticamente.
2. Controlando a serialização com atributos
public class ClienteDto
{
public int Id { get; set; }
[JsonPropertyName("nomeCompleto")] // nome customizado no JSON
public string Nome { get; set; } = string.Empty;
[JsonIgnore] // nunca serializado — nunca sai no JSON
public string SenhaHash { get; set; } = string.Empty;
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Telefone { get; set; } // omitido do JSON só quando for null
}
O mesmo cuidado do capítulo 4 do Módulo 3 (nunca expor a entidade de domínio direto) vale em dobro numa API: um DTO mal desenhado pode vazar dados sensíveis (hash de senha, campos internos) simplesmente por serem propriedades públicas da entidade que acabou sendo serializada sem filtro.
3. Records — DTOs mais concisos e imutáveis
Um record (C# 9+) é ideal para DTOs — sintaxe mais curta que uma
classe, e imutável por padrão:
public record ClienteDto(int Id, string Nome, string Email);
// equivalente à classe tradicional, mas em uma linha,
// com igualdade por valor (dois records com os mesmos dados são "iguais")
var dto1 = new ClienteDto(1, "Ana", "ana@x.com");
var dto2 = new ClienteDto(1, "Ana", "ana@x.com");
Console.WriteLine(dto1 == dto2); // true — records comparam por valor, classes por referência
4. Versionamento de API
Quando uma API já tem clientes em produção (apps mobile publicados, integrações de terceiros), mudar um contrato existente quebra quem já depende dele. Versionamento permite evoluir sem quebrar o que já funciona.
| Estratégia | Exemplo | Observação |
|---|---|---|
| Versão na URL | /api/v1/clientes, /api/v2/clientes | Mais simples de entender e testar; a mais comum |
| Versão no header | X-Api-Version: 2 | URLs ficam "limpas", mas menos visível para quem explora a API |
| Versão no Accept header | Accept: application/json;v=2 | Mais "correto" segundo REST puro, raramente usado na prática |
[ApiController]
[Route("api/v1/clientes")]
public class ClienteV1Controller : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<ActionResult<ClienteDtoV1>> ObterPorId(int id) { ... }
}
[ApiController]
[Route("api/v2/clientes")]
public class ClienteV2Controller : ControllerBase
{
// v2 pode ter um formato de DTO diferente, sem afetar quem ainda usa v1
[HttpGet("{id:int}")]
public async Task<ActionResult<ClienteDtoV2>> ObterPorId(int id) { ... }
}
Só versione quando realmente precisar quebrar um contrato existente (remover um campo, mudar seu tipo/significado). Adicionar um campo novo opcional a um DTO geralmente não exige nova versão — a maioria dos clientes ignora campos desconhecidos no JSON.
5. Onde isso te leva
Com o contrato de dados sob controle, o próximo capítulo cobre quem pode acessar cada endpoint: autenticação (quem é você) e autorização (o que você pode fazer) com JWT.