Sobre esta aula
Na aula teórica vimos records: um tipo do C# pensado para representar dados em vez de entidades, com comparação por valor (dois records com os mesmos dados são considerados iguais) e suporte natural à imutabilidade. A partir daí chegamos a uma aplicação muito comum de records no dia a dia profissional: os DTOs (Data Transfer Objects), usados para transportar dados entre as camadas de uma aplicação sem expor a entidade real.
Nesta aula prática vamos rodar códigos prontos, observar a saída, e principalmente alterar partes desses códigos para entender por que cada decisão foi tomada. Depois, você vai resolver exercícios mais completos aplicando os conceitos.
Criando o Projeto no VS Code
Siga este roteiro para criar o projeto de console em .NET desta aula:
- Abra o Visual Studio Code.
- Crie uma pasta com o seu nome em um local do computador de que você vá se lembrar depois (ex.: Área de Trabalho ou Documentos).
- No VS Code, vá em File > Open Folder e selecione a pasta criada.
- Abra o terminal integrado em Terminal > New Terminal.
- No terminal, execute os comandos abaixo.
dotnet new console -n RecordsDTO
cd RecordsDTO
dotnet run
Todo código abaixo pode ser colado dentro do método Main
(ou substituindo o conteúdo de Program.cs, no caso dos
top-level statements). Depois de colar e alterar o código, salve o arquivo e rode
dotnet run novamente para ver o novo resultado.
Atenção à ordem no Program.cs Se o seu projeto foi criado com o SDK mais
recente, o Program.cs vem sem um Main
visível (são os chamados top-level statements). Nesse formato, o C# aceita declarar
classes e records no mesmo arquivo, mas todo código "solto" (as instruções dos exemplos)
precisa vir antes de qualquer class ou
record. Se uma declaração aparecer no meio do arquivo,
antes de alguma instrução solta, o compilador acusa o erro
top-level statements must precede namespace and type declarations.
Por isso: cole cada record/class
declarado nos exemplos sempre por último, depois de todo o código de uso.
Exemplo 1 — Record x classe comum: comparação por valor
A diferença mais visível entre um record e uma
class comum aparece na comparação com
==. Rode os dois blocos abaixo, um de cada vez.
// Com record
public record Cliente(string Nome, string Email);
Cliente c1 = new("João", "joao@email.com");
Cliente c2 = new("João", "joao@email.com");
Console.WriteLine(c1 == c2); // ?
// Com uma classe comum
public class ClienteClasse
{
public string Nome { get; set; }
public string Email { get; set; }
}
ClienteClasse cc1 = new() { Nome = "João", Email = "joao@email.com" };
ClienteClasse cc2 = new() { Nome = "João", Email = "joao@email.com" };
Console.WriteLine(cc1 == cc2); // ?
Desafios:
- Antes de rodar cada bloco, escreva num papel (ou comentário) o que você espera que apareça no console. Depois compare com o resultado real.
- Mude apenas o
Emaildec2e rode de novo o bloco do record. O resultado muda? Faz sentido com a ideia de "comparação por valor"? - No bloco da classe, crie uma terceira variável
ClienteClasse cc3 = cc1;e comparecc1 == cc3. Por que esse resultado é diferente do decc1 == cc2, mesmo com os mesmos dados?
Pense e responda:
- Um
recordpor baixo dos panos ainda é uma classe (referência). Se a comparação por valor não vem do tipo em si, de onde ela vem? (dica: pesquise o que o compilador gera automaticamente para um record). - Em que situação real você diria que dois objetos são "iguais" só porque têm os mesmos dados, mesmo sendo instâncias diferentes? E em que situação isso seria um erro grave?
Exemplo 2 — Imutabilidade e o operador with
Um record criado com sintaxe posicional (como Cliente(string Nome, string Email))
tem suas propriedades somente leitura por padrão. Para "alterar" um valor, criamos uma
nova instância a partir da antiga, usando with.
public record Produto(string Nome, decimal Preco);
Produto produto = new("Notebook", 3000);
Produto produtoComDesconto = produto with
{
Preco = 2700
};
Console.WriteLine($"produto: {produto.Nome} - {produto.Preco}");
Console.WriteLine($"produtoComDesconto: {produtoComDesconto.Nome} - {produtoComDesconto.Preco}");
Console.WriteLine(ReferenceEquals(produto, produtoComDesconto));
Desafios:
- Tente escrever
produto.Preco = 2700;diretamente (semwith). O que o compilador acusa? Leia a mensagem de erro com atenção. - Use
withpara criar um segundo produto derivado deprodutoComDesconto, mudando apenas oNome. Confira sePrecofoi preservado. - Adicione ao record uma terceira propriedade,
Estoque(int). Crie um produto e, com um únicowith, gere uma cópia alterando duas propriedades ao mesmo tempo.
Pense e responda:
- O código imprime o resultado de
ReferenceEquals(produto, produtoComDesconto). O que você espera que seja impresso, e por quê? Isso contradiz ou reforça o que vimos no Exemplo 1 sobre==? - A teoria descreveu isso como imutabilidade funcional: em vez de "alterar o objeto", "a partir desse objeto, produzir outro objeto". Dê um exemplo do seu dia a dia (fora da programação) onde essa distinção — modificar algo x criar uma nova versão a partir dele — faz diferença prática.
Exemplo 3 — Por que não expor a entidade direto na API?
Imagine uma tabela de banco de dados CLIENTE com colunas
ID, NOME, EMAIL, CPF e SENHA. A
aplicação carrega essa linha como uma entidade:
public class Cliente
{
public int Id { get; set; }
public string Nome { get; set; }
public string Email { get; set; }
public string CPF { get; set; }
public string Senha { get; set; }
}
Cliente cliente = new Cliente
{
Id = 10,
Nome = "João",
Email = "joao@email.com",
CPF = "12345678900",
Senha = "123456"
};
Agora suponha que essa entidade é devolvida diretamente como resposta de uma API que lista clientes.
Rode o código acima e imagine o JSON que sairia se cliente
fosse serializado e devolvido como está, sem nenhum tratamento. Escreva esse JSON (pode ser em um
comentário no código).
Desafios:
- Liste, nesse JSON que você escreveu, quais campos nunca deveriam sair da API. Justifique cada um.
- Crie um
record ClienteDTO(string Nome, string Email)e uma variáveldtomontada manualmente a partir decliente. Imprimadtoe compare com o que a API deveria realmente devolver.
Pense e responda:
- A teoria listou vários problemas de simplesmente retornar a entidade: expor dados sensíveis, acoplar a API ao modelo do banco, dificultar mudanças, riscos de segurança, vazamento de detalhes internos. Escolha dois desses problemas e explique, com suas palavras, um cenário concreto em que cada um causaria dor de cabeça em um sistema real.
- Se um dia a coluna
SENHAvirarSENHA_HASHno banco, o que muda para quem consome a API — se ela usaClienteDTO? E se ela devolvesseClientedireto?
Exemplo 4 — Mapeamento: Entidade → DTO
Mapeamento é o nome que damos ao processo de transformar um objeto em outro — aqui, uma entidade em um DTO. Uma forma comum é isolar essa transformação em um método.
public record ClienteDTO(string Nome, string Email);
public static ClienteDTO ToDTO(Cliente cliente)
{
return new ClienteDTO(
cliente.Nome,
cliente.Email
);
}
// Uso
ClienteDTO dto = ToDTO(cliente);
Console.WriteLine($"{dto.Nome} - {dto.Email}");
Desafios:
- Adicione ao
ClienteDTOum novo campo,Iniciais(as iniciais do nome), que não existe na entidadeCliente. AjusteToDTOpara calcular esse valor. Isso ainda é "só copiar campos"? - Crie uma segunda entidade,
Produto(reaproveite a do Exemplo 2, adicionandoEstoque), e umProdutoDTOque expõe sóNomeePreco— semEstoque. Escreva o métodoToDTOcorrespondente.
Pense e responda:
- Por que colocar o mapeamento em um método próprio (
ToDTO) é melhor do que escrevernew ClienteDTO(cliente.Nome, cliente.Email)espalhado em vários lugares do código toda vez que for necessário?
Exemplo 5 — DTO também na entrada
DTOs não servem só para devolver dados. Uma API que recebe dados (por exemplo, para
cadastrar um cliente novo) também não deveria receber a entidade completa — o cliente ainda não tem
Id, e certamente não deveria poder enviar uma
Senha em texto puro pronta para ser gravada sem tratamento.
public record CriarClienteDTO(string Nome, string Email);
public static Cliente ParaEntidade(CriarClienteDTO dto)
{
return new Cliente
{
Nome = dto.Nome,
Email = dto.Email
};
}
// Simulando o que a API "recebeu"
CriarClienteDTO entrada = new("Maria", "maria@email.com");
Cliente novoCliente = ParaEntidade(entrada);
Console.WriteLine($"Cliente criado: {novoCliente.Nome} ({novoCliente.Email}), Id ainda = {novoCliente.Id}");
Desafios:
- Note que
novoCliente.Idsai com o valor padrão (0). Onde, numa aplicação real, esseIdseria efetivamente definido? Escreva a resposta em um comentário. - Crie
AtualizarClienteDTO, usado para editar um cliente existente. Pense em quais campos ele precisa ter — é igual aoCriarClienteDTOou é diferente? Justifique.
Pense e responda:
- Agora temos dois sentidos de mapeamento:
DTO → Entidade(entrada) eEntidade → DTO(saída). Por que faz sentido ter DTOs diferentes para criar, atualizar e listar um mesmo tipo de dado, em vez de um único DTO "genérico" para tudo?
Exercícios
Agora é sua vez de construir uma solução mais completa combinando record, imutabilidade, DTO e mapeamento.
Exercício 1 — Catálogo com preços promocionais
Usando o record Produto(string Nome, decimal Preco, int Estoque),
crie uma List<Produto> com pelo menos 4 produtos. Escreva
um método List<Produto> AplicarDesconto(List<Produto> produtos, decimal percentual)
que devolve uma nova lista, com cada produto tendo o preço reduzido pelo percentual informado
— usando with, sem alterar os produtos originais. Ao final,
imprima as duas listas (original e com desconto) e confirme que a original não mudou.
Exercício 2 — API de funcionários com DTO
Crie a entidade (classe comum) Funcionario com
Id, Nome,
Salario e CPF. Crie o
record FuncionarioDTO(string Nome), pensado para uma tela
pública que lista apenas nomes (sem salário nem CPF). Escreva o método de mapeamento
ToDTO e aplique-o a uma lista de funcionários com
Select (LINQ) ou um foreach.
Se ainda não usaram Select em aula, o
foreach resolve perfeitamente — o importante aqui é o
mapeamento, não o LINQ.
Exercício 3 — Cadastro de pedido de ponta a ponta (desafio)
Modele um fluxo completo de "entrada → entidade → DTO de saída":
record CriarPedidoDTO(string ClienteNome, decimal Valor)— o que a API recebe.class Pedido(entidade) comId,ClienteNome,ValoreStatus(string, iniciando como"Pendente").record PedidoDTO(int Id, string ClienteNome, decimal Valor, string Status)— o que a API devolve depois de criar o pedido.
Escreva os dois métodos de mapeamento (CriarPedidoDTO → Pedido e
Pedido → PedidoDTO) e simule o fluxo completo: receber o DTO de
entrada, "salvar" o pedido atribuindo manualmente um Id, e
devolver o PedidoDTO de saída.
Não existe uma única resposta certa aqui — o objetivo é você decidir a forma dos DTOs e do mapeamento e justificar por que cada campo está (ou não está) em cada um deles. Esteja preparado(a) para explicar sua solução.
Para refletir
Depois de resolver os exercícios, volte ao que vimos na teoria: record não é apenas uma forma mais curta de escrever uma classe, e DTO não é apenas "mais um objeto". Pense em pelo menos uma tela ou endpoint de um sistema que você já usou (rede social, e-commerce, app bancário) em que a informação exibida claramente não é tudo o que o banco de dados guarda sobre aquele registro — o que foi provavelmente escondido ou reorganizado no caminho, e por quê? E lembre-se do fechamento da teoria: record é útil para representar dados e valores, não necessariamente toda entidade do sistema; a decisão de usar record, classe, entidade ou DTO depende do papel que aquele objeto cumpre no código.