Modelagem, segurança e transferência de dados usando DTOs
Aplicações corporativas raramente trabalham com uma única representação dos dados. Um sistema pode possuir uma representação adequada ao banco de dados, outra voltada às regras de negócio e uma terceira especificamente projetada para a comunicação com uma API ou interface de usuário. Essa separação permite que diferentes partes do software evoluam de maneira relativamente independente, reduzindo o acoplamento entre infraestrutura, regras de negócio e apresentação (Sommerville, 2011; Fowler, 2002).
Nesse contexto surge o Data Transfer Object (DTO), ou Objeto de Transferência de Dados. Seu objetivo principal é representar os dados que precisam atravessar uma fronteira do sistema, como a comunicação entre um cliente e uma API, sem necessariamente expor diretamente as entidades internas do domínio.
Fowler apresenta o DTO como “an object that carries data between processes in order to reduce the number of method calls”, isto é, um objeto que transporta dados entre processos com o propósito original de reduzir a quantidade de chamadas necessárias (Fowler, 2003, tradução nossa). Com a evolução das aplicações Web e das APIs, o DTO também passou a ser utilizado amplamente como mecanismo de definição de contratos de entrada e saída, controle de informações expostas e desacoplamento arquitetural.
É importante estabelecer desde o início uma distinção conceitual: DTO não é uma entidade do domínio. Uma entidade representa um conceito significativo do negócio e pode possuir identidade, estado e comportamentos associados às regras do domínio. O DTO, por outro lado, representa essencialmente uma estrutura utilizada para transportar dados entre partes da aplicação (Evans, 2003; Fowler, 2002).
Neste artigo, construiremos os exemplos a partir de um pequeno sistema de atendimento de restaurante. Um cliente ocupa uma mesa, realiza um pedido e adiciona itens como pratos e bebidas. A partir desse cenário serão discutidos entidades, DTOs de entrada e saída, validação, segurança, mapeamento e os principais antipadrões relacionados ao uso de DTOs.
Breve histórico e o que é um DTO
O uso de objetos destinados à transferência de dados tornou-se particularmente importante em aplicações distribuídas. Quando diferentes componentes de um sistema precisam comunicar-se por uma rede, cada chamada remota possui um custo maior do que uma simples chamada de método dentro do mesmo processo.
Fowler (2002) sistematizou o Data Transfer Object entre os padrões destinados a aplicações corporativas distribuídas. Em sua formulação original, diversos valores poderiam ser agrupados em um único objeto e transmitidos de uma vez, reduzindo chamadas remotas e simplificando a comunicação.
Atualmente, DTOs aparecem com frequência em arquiteturas baseadas em APIs REST. Considere uma aplicação de restaurante na qual o frontend envia os dados necessários para registrar um pedido:
{
"nomeCliente": "Mariana",
"numeroMesa": 12,
"itens": [
{
"nomeProduto": "Filé com fritas",
"precoUnitario": 42.90,
"quantidade": 1
}
]
}Esse JSON não precisa representar diretamente uma entidade armazenada no banco. Ele representa apenas o contrato de entrada da operação de criação de um pedido.
Em C#, esse contrato pode ser modelado por meio de um DTO:
public sealed record CriarItemPedidoRequestDto
{
public required string NomeProduto { get; init; }
public decimal PrecoUnitario { get; init; }
public int Quantidade { get; init; }
}
public sealed record CriarPedidoRequestDto
{
public required string NomeCliente { get; init; }
public int NumeroMesa { get; init; }
public IReadOnlyList<CriarItemPedidoRequestDto> Itens { get; init; }
= [];
}O DTO possui dados, mas não contém regras para calcular o valor de um pedido, aplicar descontos, efetuar pagamentos ou controlar o ciclo de vida do atendimento. Essas responsabilidades pertencem ao domínio.
O problema que o DTO resolve
Uma abordagem aparentemente simples seria utilizar diretamente uma entidade como parâmetro de uma API:
[HttpPost]
public IActionResult Criar(Pedido pedido)
{
// salvar pedido
return Ok();
}Embora funcione tecnicamente em aplicações pequenas, essa solução cria acoplamento entre o contrato externo da API e a estrutura interna do domínio.
Suponha que a entidade Pedido possua:
public class Pedido
{
public int Id { get; set; }
public string NomeCliente { get; set; } = "";
public int NumeroMesa { get; set; }
public bool Pago { get; set; }
public decimal DescontoPercentual { get; set; }
public DateTime CriadoEm { get; set; }
}Se essa mesma classe for utilizada diretamente pela API, o cliente poderá conhecer e eventualmente enviar todos esses campos.
Um usuário poderia tentar enviar:
{
"nomeCliente": "Mariana",
"numeroMesa": 12,
"pago": true,
"descontoPercentual": 100
}Entretanto, um cliente comum não deveria decidir que uma conta está paga nem aplicar um desconto de 100%.
Um DTO específico resolve parte desse problema:
public sealed record CriarPedidoRequestDto
{
public required string NomeCliente { get; init; }
public int NumeroMesa { get; init; }
}Agora, Pago e DescontoPercentual simplesmente não fazem parte do contrato permitido para essa operação.
O DTO funciona, portanto, como uma fronteira explícita entre aquilo que o cliente pode fornecer e aquilo que pertence internamente ao domínio da aplicação.
Entidade de domínio versus DTO
Em Domain-Driven Design (DDD), uma entidade possui identidade própria e continua representando o mesmo conceito ao longo de mudanças de estado. Além dos dados, entidades podem encapsular comportamentos e invariantes relevantes ao negócio (Evans, 2003).
No restaurante, um Pedido é um bom exemplo de entidade. Mesmo que novos itens sejam adicionados ou que o pagamento seja realizado, trata-se do mesmo pedido.
Uma implementação simplificada poderia ser:
public sealed class Pedido
{
private readonly List<ItemPedido> _itens = [];
public int Id { get; }
public string NomeCliente { get; private set; }
public int NumeroMesa { get; private set; }
public IReadOnlyCollection<ItemPedido> Itens =>
_itens.AsReadOnly();
public bool Pago { get; private set; }
public decimal DescontoPercentual { get; private set; }
public Pedido(
int id,
string nomeCliente,
int numeroMesa)
{
if (string.IsNullOrWhiteSpace(nomeCliente))
throw new ArgumentException(
"O nome do cliente é obrigatório.");
if (numeroMesa <= 0)
throw new ArgumentException(
"O número da mesa deve ser positivo.");
Id = id;
NomeCliente = nomeCliente;
NumeroMesa = numeroMesa;
}
public void AdicionarItem(
string nomeProduto,
decimal precoUnitario,
int quantidade)
{
_itens.Add(
new ItemPedido(
nomeProduto,
precoUnitario,
quantidade));
}
public void AplicarDesconto(decimal percentual)
{
if (percentual < 0 || percentual > 30)
throw new ArgumentException(
"O desconto deve estar entre 0% e 30%.");
DescontoPercentual = percentual;
}
public void MarcarComoPago()
{
Pago = true;
}
public decimal CalcularTotal()
{
decimal subtotal = _itens.Sum(
item => item.CalcularSubtotal());
decimal desconto =
subtotal * DescontoPercentual / 100m;
return subtotal - desconto;
}
}O item do pedido também pode conter regras próprias:
public sealed class ItemPedido
{
public string NomeProduto { get; }
public decimal PrecoUnitario { get; }
public int Quantidade { get; }
public ItemPedido(
string nomeProduto,
decimal precoUnitario,
int quantidade)
{
if (string.IsNullOrWhiteSpace(nomeProduto))
throw new ArgumentException(
"O nome do produto é obrigatório.");
if (precoUnitario <= 0)
throw new ArgumentException(
"O preço deve ser maior que zero.");
if (quantidade <= 0)
throw new ArgumentException(
"A quantidade deve ser maior que zero.");
NomeProduto = nomeProduto;
PrecoUnitario = precoUnitario;
Quantidade = quantidade;
}
public decimal CalcularSubtotal()
{
return PrecoUnitario * Quantidade;
}
}Observe que essas classes não são simples contêineres de dados. Elas protegem regras importantes do domínio.
A Microsoft recomenda exatamente essa separação em arquiteturas orientadas ao domínio: entidades devem preservar seus invariantes e comportamentos, enquanto os modelos utilizados pelas APIs podem possuir outra representação (Microsoft, 2026a).
Podemos resumir conceitualmente:
| Aspecto | Entidade de domínio | DTO |
|---|---|---|
| Representa | Conceito do negócio | Dados transferidos |
| Possui identidade | Frequentemente | Normalmente não |
| Possui regras de negócio | Sim | Não deveria |
| Pode alterar estado | Sim | Preferencialmente não |
| Deve refletir o banco | Não obrigatoriamente | Não |
| Usado como contrato de API | Não recomendado | Sim |
| Objetivo principal | Modelar o domínio | Transportar dados |
Casos de uso dos DTOs
Otimização de performance e redução de payload
Um dos propósitos históricos do DTO é transportar apenas os dados necessários para determinada operação (Fowler, 2002).
Suponha uma entidade Pedido contendo dezenas de atributos internos. Uma tela que apresenta apenas os pedidos em andamento talvez precise somente de:
{
"id": 47,
"numeroMesa": 12,
"total": 78.90
}Não existe necessidade de transmitir todas as propriedades internas da entidade.
Um DTO específico poderia ser:
public sealed record ResumoPedidoResponseDto(
int Id,
int NumeroMesa,
decimal Total
);Uma coleção com 100 pedidos pode, dessa maneira, transportar significativamente menos informações do que uma serialização completa das entidades.
O princípio é simples:
o contrato deve conter os dados necessários para aquele caso de uso, e não automaticamente tudo o que existe no domínio.
Além da redução do payload HTTP, essa estratégia reduz o acoplamento entre o cliente e a estrutura interna do servidor.
Segurança e proteção contra exposição de dados sensíveis
DTOs também ajudam a controlar informações retornadas por uma API. Considere que uma entidade possua informações internas:
public sealed class Cliente
{
public int Id { get; set; }
public string Nome { get; set; } = "";
public string Cpf { get; set; } = "";
public string ObservacaoInterna { get; set; } = "";
}Uma API que simplesmente retorna a entidade:
return Ok(cliente);
poderia expor informações que não deveriam chegar ao frontend.
Um DTO limita explicitamente a resposta:
public sealed record ClienteResponseDto(
int Id,
string Nome
);O objeto retornado contém somente:
{
"id": 18,
"nome": "Carlos"
}Isso aplica o princípio de minimização da exposição de informações: cada consumidor recebe somente aquilo que necessita.
É importante observar, entretanto, que DTOs não substituem autenticação e autorização. Um DTO controla quais propriedades aparecem no contrato, mas continua sendo responsabilidade da aplicação decidir quem pode executar cada operação.
Prevenção de ataques de Mass Assignment
O Mass Assignment ocorre quando mecanismos automáticos de binding atribuem dados controlados pelo usuário diretamente às propriedades de um objeto sem restringir adequadamente quais campos podem ser modificados.
A OWASP destaca que esse problema pode permitir a alteração de propriedades que o desenvolvedor nunca pretendia disponibilizar ao usuário e recomenda, entre as estratégias de defesa, o uso de DTOs contendo somente os campos permitidos (OWASP, 2026).
Considere novamente:
public class Pedido
{
public string NomeCliente { get; set; } = "";
public int NumeroMesa { get; set; }
public bool Pago { get; set; }
public decimal DescontoPercentual { get; set; }
}E um endpoint:
[HttpPost]
public IActionResult Criar(Pedido pedido)
{
// ...
}Um cliente mal-intencionado poderia tentar:
{
"nomeCliente": "João",
"numeroMesa": 8,
"pago": true,
"descontoPercentual": 100
}O DTO restringe o conjunto de propriedades aceitas:
public sealed record CriarPedidoRequestDto
{
public required string NomeCliente { get; init; }
public int NumeroMesa { get; init; }
}Agora não existem propriedades Pago ou DescontoPercentual no objeto de entrada. Esse comportamento equivale a uma estratégia de allow-list: somente os campos explicitamente definidos como parte daquela operação podem ser recebidos.
Porém, deve-se observar novamente que:
DTO não substitui autorização.
Mesmo que exista um DTO como:
public sealed record AplicarDescontoRequestDto
{
public decimal Percentual { get; init; }
}a API ainda precisa verificar se o usuário autenticado possui permissão para aplicar descontos.
Padrões de design
Desacoplamento entre banco de dados e API
Uma aplicação possui, frequentemente, três representações diferentes:
Banco de dados
↓
Entidade / Modelo de domínio
↓
Data Transfer Object (DTO)
↓
API / Cliente
Essas representações não precisam possuir exatamente os mesmos campos.
Considere uma tabela:
PEDIDO
--------------------------------
id
nome_cliente
numero_mesa
desconto_percentual
pago
criado_em
usuario_responsavel_id
A API utilizada por um cliente do restaurante talvez precise retornar apenas:
{
"id": 35,
"numeroMesa": 7,
"total": 86.50
}Se o sistema expõe diretamente sua representação de persistência, uma mudança no banco pode produzir alterações indesejadas na API.
Fowler (2002) enfatiza que diferentes representações de dados podem ser separadas por mecanismos de mapeamento. Em arquiteturas em camadas, essa separação reduz o impacto causado pela evolução de uma camada sobre as demais.
Assim:
Estrutura do banco ≠ Entidade ≠ DTO
Essa desigualdade não representa um erro. Em muitos sistemas, ela é justamente o resultado de uma arquitetura adequadamente desacoplada.
Request DTO e Response DTO
Um erro frequente consiste em considerar que existe apenas “o DTO” de determinada entidade. Entretanto, operações diferentes possuem necessidades diferentes.
No restaurante, para criar um pedido, o cliente pode enviar:
public sealed record CriarPedidoRequestDto
{
public required string NomeCliente { get; init; }
public int NumeroMesa { get; init; }
public IReadOnlyList<CriarItemPedidoRequestDto> Itens { get; init; } = [];
}Esse é um Request DTO.
Para responder à requisição, entretanto, a API pode enviar:
public sealed record PedidoResponseDto(
int Id,
string NomeCliente,
int NumeroMesa,
IReadOnlyList<ItemPedidoResponseDto> Itens,
decimal Total,
bool Pago
);Com:
public sealed record ItemPedidoResponseDto(
string NomeProduto,
int Quantidade,
decimal PrecoUnitario,
decimal Subtotal
);O resultado poderia ser:
{
"id": 25,
"nomeCliente": "Mariana",
"numeroMesa": 12,
"itens": [
{
"nomeProduto": "Filé com fritas",
"quantidade": 1,
"precoUnitario": 42.90,
"subtotal": 42.90
}
],
"total": 42.90,
"pago": false
}Observe que Id, Total e Pago aparecem na resposta, mas não necessariamente na entrada.
Isso resulta em dois contratos distintos:
CriarPedidoRequestDto
↓
API
↓
domínio
↓
API
↓
PedidoResponseDto
Essa separação melhora a clareza da intenção de cada objeto.
Validação dos dados de entrada
Um Request DTO é uma posição adequada para validar características estruturais da entrada.
O namespace System.ComponentModel.DataAnnotations disponibiliza atributos como Required, StringLength e Range, utilizados pelo ASP.NET Core durante o processo de validação do modelo (Microsoft, 2026b).
Podemos melhorar nosso DTO:
using System.ComponentModel.DataAnnotations;
public sealed record CriarItemPedidoRequestDto
{
[Required]
[StringLength(100)]
public required string NomeProduto { get; init; }
[Range(
0.01,
10000,
ErrorMessage = "O preço deve ser maior que zero.")]
public decimal PrecoUnitario { get; init; }
[Range(
1,
100,
ErrorMessage = "A quantidade deve estar entre 1 e 100.")]
public int Quantidade { get; init; }
}E:
using System.ComponentModel.DataAnnotations;
public sealed record CriarPedidoRequestDto
{
[Required]
[StringLength(100, MinimumLength = 2)]
public required string NomeCliente { get; init; }
[Range(
1,
200,
ErrorMessage = "A mesa deve estar entre 1 e 200.")]
public int NumeroMesa { get; init; }
[MinLength(
1,
ErrorMessage = "O pedido deve possuir pelo menos um item.")]
public IReadOnlyList<CriarItemPedidoRequestDto> Itens { get; init; }
= [];
}Em um controller ASP.NET Core:
[ApiController]
[Route("api/pedidos")]
public class PedidosController : ControllerBase
{
[HttpPost]
public IActionResult Criar(
CriarPedidoRequestDto dto)
{
return Ok(dto);
}
}Quando [ApiController] é utilizado, o ASP.NET Core integra a validação ao processamento das requisições e pode retornar automaticamente uma resposta HTTP 400 quando o modelo recebido não satisfaz as regras declaradas (Microsoft, 2026c).
Por exemplo:
{
"nomeCliente": "",
"numeroMesa": 0,
"itens": []
}deve ser rejeitado antes de se tornar uma entidade válida do domínio.
É importante destacar que a validação de entrada não substitui regras de domínio. Existe uma distinção importante entre Validação da entrada e Regra de negócio.
Uma validação no DTO pode verificar:
numeroMesa > 0
porque um valor negativo não é estruturalmente adequado.
Entretanto, uma regra como:
somente o gerente pode conceder desconto maior que 10%
é uma regra de negócio e não deveria ficar dentro do DTO.
A documentação arquitetural do .NET distingue explicitamente esses dois níveis: validações de campos podem ocorrer em DTOs de entrada, enquanto invariantes essenciais devem continuar protegidas no modelo de domínio (Microsoft, 2022).
Portanto, mesmo que exista:
[Range(0, 30)]
public decimal Percentual { get; init; }a entidade ainda pode proteger sua própria regra:
public void AplicarDesconto(decimal percentual)
{
if (percentual < 0 || percentual > 30)
throw new ArgumentException(
"Desconto inválido.");
DescontoPercentual = percentual;
}Essa redundância tem finalidades diferentes:
DTO
↓
Protege e valida o contrato de entrada.
DOMÍNIO
↓
Protege as regras essenciais do negócio.
Uma entidade não deveria depender da existência de determinada API para continuar válida.
Imutabilidade e uso de records
DTOs normalmente representam um conjunto de dados recebido ou produzido em determinado momento. Por isso, a imutabilidade é frequentemente desejável.
C# oferece record, um tipo especialmente apropriado para estruturas cujo papel principal é armazenar dados. Records possuem igualdade baseada em valores e oferecem uma sintaxe concisa, além de favorecerem modelos imutáveis (Microsoft, 2026d).
Compare:
public class PedidoDto
{
public int Id { get; set; }
public string Cliente { get; set; } = "";
}com:
public sealed record PedidoDto(
int Id,
string Cliente
);Uso:
var pedido = new PedidoDto(
10,
"Mariana");As propriedades do record posicional não podem simplesmente receber novas atribuições:
// Não permitido:
// pedido.Id = 20;Caso seja necessário derivar outro valor, C# oferece a expressão with:
var outroPedido =
pedido with
{
Cliente = "Carlos"
};O objeto original permanece:
PedidoDto { Id = 10, Cliente = Mariana }
e é produzido outro objeto:
PedidoDto { Id = 10, Cliente = Carlos }
DTOs imutáveis reduzem modificações acidentais depois que o objeto foi construído e deixam mais explícito o seu papel de transportar um estado.
Deve-se observar, contudo, que records não são automaticamente a melhor escolha para entidades persistidas pelo Entity Framework Core. A própria documentação da Microsoft alerta que entidades rastreadas pelo EF Core dependem de semântica de identidade e igualdade por referência, enquanto records possuem igualdade baseada em valores (Microsoft, 2026d).
Essa diferença reforça a separação:
Entidade → identidade
DTO → valores transportados
Mapeamento entre DTO e entidade
Como entidade e DTO representam responsabilidades diferentes, algum ponto da aplicação precisa transformar um no outro. Essa operação é chamada de mapeamento.
O fluxo de criação de um pedido pode ser representado assim:
JSON
↓
CriarPedidoRequestDto
↓
MAPEAMENTO
↓
Pedido
↓
regras de negócio
↓
MAPEAMENTO
↓
PedidoResponseDto
↓
JSON
Fowler (2002) já relacionava DTOs com objetos responsáveis por transferir informações entre representações distintas. Em arquiteturas modernas, essa conversão costuma ocorrer na camada de aplicação ou em classes especializadas de mapeamento.
Mapeamento manual
Para aplicações pequenas e exemplos didáticos, o mapeamento manual é normalmente a alternativa mais clara.
Podemos criar:
public static class PedidoMapper
{
public static Pedido ParaEntidade(
CriarPedidoRequestDto dto,
int id)
{
var pedido = new Pedido(
id,
dto.NomeCliente,
dto.NumeroMesa);
foreach (var itemDto in dto.Itens)
{
pedido.AdicionarItem(
itemDto.NomeProduto,
itemDto.PrecoUnitario,
itemDto.Quantidade);
}
return pedido;
}
public static PedidoResponseDto ParaResponse(
Pedido pedido)
{
var itens = pedido.Itens
.Select(item =>
new ItemPedidoResponseDto(
item.NomeProduto,
item.Quantidade,
item.PrecoUnitario,
item.CalcularSubtotal()))
.ToList();
return new PedidoResponseDto(
pedido.Id,
pedido.NomeCliente,
pedido.NumeroMesa,
itens,
pedido.CalcularTotal(),
pedido.Pago);
}
}Agora:
var dto = new CriarPedidoRequestDto
{
NomeCliente = "Mariana",
NumeroMesa = 12,
Itens =
[
new CriarItemPedidoRequestDto
{
NomeProduto = "Filé com fritas",
PrecoUnitario = 42.90m,
Quantidade = 1
},
new CriarItemPedidoRequestDto
{
NomeProduto = "Suco de laranja",
PrecoUnitario = 9.00m,
Quantidade = 2
}
]
};Pode ser convertido:
Pedido pedido =
PedidoMapper.ParaEntidade(
dto,
id: 1);E posteriormente:
PedidoResponseDto resposta =
PedidoMapper.ParaResponse(pedido);O total será:
42,90 + (9,00 × 2)
42,90 + 18,00
60,90
O mapeamento manual possui algumas vantagens:
- é explícito;
- facilita a leitura por iniciantes;
- não introduz dependências externas;
- permite identificar claramente quais campos atravessam cada fronteira;
- facilita o tratamento de conversões especiais.
Sua principal desvantagem aparece em sistemas muito grandes, nos quais centenas de propriedades e DTOs podem gerar código repetitivo.
Onde deve ocorrer o mapeamento?
Uma entidade de domínio não deveria conhecer detalhes sobre uma API REST. Portanto, algo como:
public PedidoResponseDto ConverterParaDto()
{
...
}
dentro da própria entidade geralmente cria uma dependência indesejada. A entidade passa a conhecer uma representação pertencente a outra camada.
Uma organização melhor seria:
Presentation/API
↓
Application
↓
Domain
↓
Infrastructure
O mapeamento entre objetos de API e objetos de domínio pode ocorrer na camada de aplicação ou em componentes especializados utilizados por ela.
A documentação arquitetural da Microsoft reforça que entidades do modelo de domínio não devem ser propagadas diretamente para camadas às quais não pertencem; quando diferentes camadas usam representações distintas, torna-se necessária uma tradução entre esses tipos (Microsoft, 2026a).
Uma possível organização seria:
Restaurante.Api
├── Controllers
└── Dtos
├── Requests
└── Responses
Restaurante.Application
└── Mappers
Restaurante.Domain
├── Pedido.cs
└── ItemPedido.cs
Restaurante.Infrastructure
└── Persistencia
Assim:
Domain
não precisa conhecer:
PedidoResponseDto
nem:
CriarPedidoRequestDto
Mapeamento automático
Quando existem dezenas ou centenas de classes, escrever cada conversão manualmente pode produzir bastante código repetitivo. Por isso existem bibliotecas capazes de automatizar parte do mapeamento entre objetos.
Conceitualmente, em vez de escrever:
var resposta =
new PedidoResponseDto(
pedido.Id,
pedido.NomeCliente,
pedido.NumeroMesa,
itens,
pedido.CalcularTotal(),
pedido.Pago);uma biblioteca pode receber configurações que relacionem propriedades entre os tipos.
Ferramentas desse tipo podem ser úteis quando existem muitos mapeamentos triviais, como:
Nome → Nome
Idade → Idade
Email → Email
Entretanto, a automação apresenta uma desvantagem pedagógica e arquitetural: parte da transformação fica menos explícita no código. Para sistemas pequenos, ou quando a transformação envolve regras importantes, o mapeamento manual tende a ser mais transparente.
Uma heurística razoável é:
Poucos DTOs + transformações simples
→ mapeamento manual.
Muitos DTOs + grande repetição
→ considerar ferramenta de mapeamento.
Ainda assim, bibliotecas de mapeamento não devem receber a responsabilidade por regras de negócio. O mapeamento deve transformar representações, não decidir comportamentos do domínio.
Exemplo integrado com ASP.NET Core
Podemos agora integrar os conceitos em um controller simples. Considere um armazenamento em memória:
private static readonly List<Pedido> Pedidos = [];O controller poderia ser:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/pedidos")]
public class PedidosController : ControllerBase
{
private static readonly List<Pedido> Pedidos = [];
[HttpPost]
public ActionResult<PedidoResponseDto> Criar(
CriarPedidoRequestDto dto)
{
int novoId =
Pedidos.Count == 0
? 1
: Pedidos.Max(p => p.Id) + 1;
Pedido pedido =
PedidoMapper.ParaEntidade(
dto,
novoId);
Pedidos.Add(pedido);
PedidoResponseDto resposta =
PedidoMapper.ParaResponse(pedido);
return CreatedAtAction(
nameof(ObterPorId),
new { id = pedido.Id },
resposta);
}
[HttpGet("{id:int}")]
public ActionResult<PedidoResponseDto> ObterPorId(
int id)
{
Pedido? pedido =
Pedidos.FirstOrDefault(
p => p.Id == id);
if (pedido is null)
return NotFound();
return Ok(
PedidoMapper.ParaResponse(pedido));
}
}Observe o fluxo:
1. ASP.NET recebe JSON.
2. JSON é convertido em CriarPedidoRequestDto.
3. O DTO é validado.
4. PedidoMapper converte DTO em Pedido.
5. Pedido executa e protege regras do domínio.
6. A entidade é armazenada.
7. PedidoMapper converte Pedido em PedidoResponseDto.
8. ASP.NET serializa o DTO para JSON.
A entidade nunca é utilizada diretamente como contrato externo.
Exemplo completo do fluxo
A requisição na API poderia ser do tipo:
POST /api/pedidos
Content-Type: application/json
enquanto o corpo da requisição viria com o JSON:
{
"nomeCliente": "Mariana",
"numeroMesa": 12,
"itens": [
{
"nomeProduto": "Filé com fritas",
"precoUnitario": 42.90,
"quantidade": 1
},
{
"nomeProduto": "Suco de laranja",
"precoUnitario": 9.00,
"quantidade": 2
}
]
}O ASP.NET Core cria:
CriarPedidoRequestDto
que é convertido em:
Pedido
A entidade então calcula:
42,90 + 18,00 = 60,90
Finalmente, a resposta utiliza:
PedidoResponseDto
e resulta em:
{
"id": 1,
"nomeCliente": "Mariana",
"numeroMesa": 12,
"itens": [
{
"nomeProduto": "Filé com fritas",
"quantidade": 1,
"precoUnitario": 42.90,
"subtotal": 42.90
},
{
"nomeProduto": "Suco de laranja",
"quantidade": 2,
"precoUnitario": 9.00,
"subtotal": 18.00
}
],
"total": 60.90,
"pago": false
}Podemos representar toda a arquitetura da operação como:
CLIENTE
|
| JSON
v
CriarPedidoRequestDto
|
| validação
v
PedidoMapper
|
v
Pedido
|
| regras de negócio
v
PedidoMapper
|
v
PedidoResponseDto
|
| JSON
v
CLIENTE
Antipadrões
Colocar regras de negócio dentro do DTO
Considere:
public sealed record PedidoDto
{
public decimal Total { get; init; }
public decimal Desconto { get; init; }
public decimal CalcularValorFinal()
{
return Total - Desconto;
}
}O DTO começou a executar comportamento de negócio. Em seguida, alguém poderia adicionar:
public bool PodeReceberDesconto()
{
return Total > 100;
}Depois:
public void AprovarPagamento()
{
// ...
}Gradualmente o DTO passa a assumir responsabilidades do modelo de domínio. Essa abordagem mistura transporte de dados com regra de negócio.
O comportamento deve permanecer em uma entidade ou serviço apropriado do domínio. O DTO seria:
public sealed record PedidoResponseDto(
int Id,
decimal Total,
bool Pago
);enquanto a entidade ficaria como:
public void MarcarComoPago()
{
Pago = true;
}Essa separação mantém as responsabilidades mais claras.
DTO que espelha 100% da entidade
Considere uma entidade:
public class Pedido
{
public int Id { get; set; }
public string NomeCliente { get; set; } = "";
public int NumeroMesa { get; set; }
public decimal DescontoPercentual { get; set; }
public bool Pago { get; set; }
public DateTime CriadoEm { get; set; }
public int UsuarioResponsavelId { get; set; }
}E então:
public class PedidoDto
{
public int Id { get; set; }
public string NomeCliente { get; set; } = "";
public int NumeroMesa { get; set; }
public decimal DescontoPercentual { get; set; }
public bool Pago { get; set; }
public DateTime CriadoEm { get; set; }
public int UsuarioResponsavelId { get; set; }
}Nesse caso devemos perguntar:
por que o DTO existe?
Se toda alteração na entidade exige uma alteração idêntica no DTO e todas as propriedades sempre atravessam a fronteira, grande parte do desacoplamento esperado deixou de existir.
Isso não significa que dois objetos nunca possam possuir propriedades semelhantes. Existem situações legítimas em que os contratos coincidem.
O antipadrão ocorre quando a equipe cria sistematicamente:
Entidade X
+
XDto idêntico
sem avaliar a necessidade de cada caso de uso.
DTOs devem ser orientados ao contrato necessário, não simplesmente copiados da estrutura da entidade.
O DTO “Deus”
Outro problema ocorre quando um único DTO é utilizado para todas as operações:
public class PedidoDto
{
public int Id { get; set; }
public string NomeCliente { get; set; } = "";
public int NumeroMesa { get; set; }
public decimal Total { get; set; }
public bool Pago { get; set; }
public decimal Desconto { get; set; }
public DateTime CriadoEm { get; set; }
}A equipe utiliza essa classe para:
Criar pedido
Atualizar pedido
Consultar pedido
Listar pedido
Efetuar pagamento
Aplicar desconto
O problema é que cada operação possui necessidades diferentes.
Para criar:
public sealed record CriarPedidoRequestDto
{
public required string NomeCliente { get; init; }
public int NumeroMesa { get; init; }
}Para pagamento:
public sealed record RegistrarPagamentoRequestDto
{
public decimal ValorRecebido { get; init; }
}Para desconto:
public sealed record AplicarDescontoRequestDto
{
public decimal Percentual { get; init; }
}Para listagem:
public sealed record ResumoPedidoResponseDto(
int Id,
int NumeroMesa,
decimal Total
);Para detalhes:
public sealed record PedidoResponseDto(
int Id,
string NomeCliente,
int NumeroMesa,
IReadOnlyList<ItemPedidoResponseDto> Itens,
decimal Total,
bool Pago
);Esses objetos expressam claramente a intenção de cada caso de uso.
Compare:
PedidoDto
com:
CriarPedidoRequestDto
AplicarDescontoRequestDto
RegistrarPagamentoRequestDto
ResumoPedidoResponseDto
PedidoResponseDto
A segunda opção possui mais classes, mas cada classe possui uma responsabilidade muito menor e mais explícita.
Como nomear DTOs
Usar simplesmente:
PedidoDto
nem sempre comunica adequadamente a finalidade do objeto.
Quando diferentes operações possuem contratos distintos, nomes orientados ao caso de uso são mais informativos:
CriarPedidoRequestDto
AtualizarPedidoRequestDto
AplicarDescontoRequestDto
PedidoResponseDto
ResumoPedidoResponseDto
A própria nomenclatura revela:
qual operação?
+
entrada ou saída?
+
qual conceito?
Por exemplo:
CriarPedidoRequestDto
significa:
CriarPedido
→ caso de uso.
Request
→ dados entrando na aplicação.
Dto
→ objeto destinado à transferência.
Essa nomenclatura reduz ambiguidades durante a leitura do código.
Quando usar DTOs?
DTOs são particularmente úteis quando:
- existe comunicação por API;
- a representação externa não deve ser igual à representação interna;
- somente parte das informações deve ser enviada;
- dados sensíveis precisam ser ocultados;
- diferentes operações aceitam diferentes campos;
- existe risco de mass assignment;
- o domínio precisa evoluir sem quebrar automaticamente a interface externa;
- respostas precisam combinar ou transformar dados provenientes de diferentes objetos.
Entretanto, também existe um custo. Criar DTOs implica:
mais tipos;
mais mapeamentos;
mais arquivos;
mais código.
Portanto, DTO não deve ser empregado mecanicamente. Uma aplicação extremamente simples pode não justificar dezenas de DTOs. A decisão arquitetural deve considerar a complexidade e as fronteiras reais do sistema.
Fowler (2002) alerta de maneira mais ampla que padrões arquiteturais devem resolver problemas concretos, não simplesmente ser aplicados por hábito.
DTO não é sinônimo de modelagem de domínio
Esse ponto merece atenção especial. Considere:
public sealed record CriarPedidoRequestDto(
string NomeCliente,
int NumeroMesa
);Esse tipo descreve:
os dados necessários para solicitar a criação de um pedido.
Já:
public sealed class Pedido
{
// estado
// identidade
// comportamentos
// invariantes
}representa:
o conceito de Pedido dentro do negócio.
Evans (2003) propõe que o modelo de domínio expresse conceitos e regras essenciais do problema tratado pelo software. Um DTO não busca modelar profundamente o domínio; ele modela um contrato de comunicação relacionado a determinado caso de uso.
Podemos representar:
DOMÍNIO
-----------------------------------
Pedido
ItemPedido
Mesa
Pagamento
Produto
CONTRATOS
-----------------------------------
CriarPedidoRequestDto
PedidoResponseDto
AplicarDescontoRequestDto
RegistrarPagamentoRequestDto
A primeira categoria representa o negócio e a segunda representa maneiras pelas quais outras partes do sistema interagem com esse negócio.
Uma arquitetura final para o restaurante
Uma pequena aplicação poderia adotar a seguinte sugestão de arquitetura:
Restaurante
│
├── Api
│ ├── Controllers
│ │ └── PedidosController.cs
│ │
│ └── Dtos
│ ├── Requests
│ │ ├── CriarPedidoRequestDto.cs
│ │ ├── CriarItemPedidoRequestDto.cs
│ │ ├── AplicarDescontoRequestDto.cs
│ │ └── RegistrarPagamentoRequestDto.cs
│ │
│ └── Responses
│ ├── PedidoResponseDto.cs
│ ├── ItemPedidoResponseDto.cs
│ └── ResumoPedidoResponseDto.cs
│
├── Application
│ └── Mappers
│ └── PedidoMapper.cs
│
├── Domain
│ └── Entities
│ ├── Pedido.cs
│ └── ItemPedido.cs
│
└── Infrastructure
└── Persistence
O fluxo resultante seria:
HTTP Request
↓
Request DTO
↓
validação de entrada
↓
mapeamento
↓
modelo de domínio
↓
regras de negócio
↓
persistência
↓
mapeamento
↓
Response DTO
↓
HTTP Response
Essa estrutura cria fronteiras explícitas entre responsabilidades.
Conclusão
Um Data Transfer Object é um objeto destinado a transportar dados entre partes de um sistema. Embora tenha surgido em um contexto fortemente associado à redução de chamadas remotas, tornou-se também uma ferramenta importante na definição de contratos de APIs, isolamento arquitetural, controle da exposição de dados e proteção contra atribuições indevidas de propriedades (Fowler, 2002; OWASP, 2026).
A principal distinção conceitual é que entidade e DTO possuem responsabilidades diferentes. A entidade representa conceitos do domínio, possui identidade, comportamento e invariantes. O DTO representa os dados necessários para determinada comunicação ou caso de uso (Evans, 2003).
DTOs específicos de entrada ainda podem reduzir riscos de mass assignment, pois apenas propriedades explicitamente permitidas aparecem no contrato recebido. DTOs de saída, por sua vez, ajudam a evitar a exposição acidental de informações internas ou sensíveis (OWASP, 2026).
Em C# moderno, record e propriedades init oferecem mecanismos adequados à criação de objetos voltados predominantemente ao transporte de dados. A imutabilidade proporcionada por essas construções também reduz alterações acidentais no estado dos DTOs depois de criados (Microsoft, 2026d).
Por fim, o uso de DTOs deve evitar três excessos: inserir regras de negócio dentro dos objetos de transferência, reproduzir integralmente entidades sem necessidade e criar um único DTO gigantesco reutilizado por todas as operações. Bons DTOs são pequenos, explícitos e orientados ao caso de uso.
Em vez de perguntar apenas:
“Qual é o DTO da entidade Pedido?”
uma pergunta arquitetural mais adequada é:
“Quais dados esta operação precisa receber ou devolver?”
É essa mudança de perspectiva que transforma o DTO de uma simples classe auxiliar em um instrumento efetivo de design de software.
Obrigado pela leitura e bons estudos!
Referências
EVANS, Eric. Domain-driven design: tackling complexity in the heart of software. Boston: Addison-Wesley, 2003.
FOWLER, Martin. Patterns of enterprise application architecture. Boston: Addison-Wesley, 2002.
FOWLER, Martin. Data Transfer Object. [S. l.], 2003. Disponível em: https://martinfowler.com/eaaCatalog/dataTransferObject.html. Acesso em: 23 out. 2026.
MICROSOFT. Designing a DDD-oriented microservice. [S. l.]: Microsoft, 2026a. Disponível em: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice. Acesso em: 23 out. 2026.
MICROSOFT. Designing validations in the domain model layer. [S. l.]: Microsoft, 2022. Disponível em: https://learn.microsoft.com/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/domain-model-layer-validations. Acesso em: 23 out. 2026.
MICROSOFT. System.ComponentModel.DataAnnotations namespace. [S. l.]: Microsoft, 2026b. Disponível em: https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations. Acesso em: 23 out. 2026.
MICROSOFT. Model validation in ASP.NET Core MVC. [S. l.]: Microsoft, 2026c. Disponível em: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation. Acesso em: 23 out. 2026.
MICROSOFT. C# record types. [S. l.]: Microsoft, 2026d. Disponível em: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/records. Acesso em: 23 out. 2026.
OWASP FOUNDATION. Mass Assignment Cheat Sheet. [S. l.]: OWASP Foundation, 2026. Disponível em: https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html. Acesso em: 23 out. 2026.
SOMMERVILLE, Ian. Engenharia de software. 9. ed. São Paulo: Pearson Prentice Hall, 2011.


