Records, Imutabilidade e DTOs - Aula Prática

Programação Aplicada em .NET • Prof. Me. William Tenório

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:

  1. Abra o Visual Studio Code.
  2. 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).
  3. No VS Code, vá em File > Open Folder e selecione a pasta criada.
  4. Abra o terminal integrado em Terminal > New Terminal.
  5. 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:

Pense e responda:

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:

Pense e responda:

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:

Pense e responda:

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:

Pense e responda:

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:

Pense e responda:

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":

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.