Módulo 3 · Arquitetura MVC — Capítulo 04

Models, ViewModels e DTOs

Três nomes parecidos, três propósitos diferentes: por que a entidade que representa uma tabela raramente é a mesma classe que uma tela deveria receber.

1. O problema de usar a entidade direto na tela

A tentação inicial é passar a entidade de domínio (a classe que espelha a tabela Cliente do Módulo 2) diretamente para a View. Funciona para telas simples, mas quebra rápido: uma tela de edição não precisa de DataCadastro; um formulário de cadastro precisa de um campo "Confirmar senha" que não existe na tabela; uma tela de listagem quer mostrar "Total de Pedidos", um valor calculado que não é uma coluna. Três tipos diferentes de classe resolvem essas necessidades diferentes.

Model (entidade)ViewModelDTO
RepresentaUma tabela/conceito de domínioO que uma tela específica precisaDados trafegando entre camadas (ex: API ↔ cliente)
Onde viveCamada de domínio/dadosCamada de apresentação (MVC)Camada de API/integração
Pode ter campos calculados/agregados?RaramenteSim, com frequênciaSim
Pode ter campos "de formulário" (ex: ConfirmarSenha)?NãoSimRaramente

2. Model (entidade de domínio)

C# Cliente.cs — espelha a tabela do Módulo 2
public class Cliente
{
    public int Id { get; set; }
    public string Nome { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public bool Ativo { get; set; }
    public DateTime DataCadastro { get; set; }
}

3. ViewModel — moldado para uma tela específica

C# ClienteListaViewModel.cs
// para a tela de LISTAGEM — inclui um dado calculado que não existe na tabela
public class ClienteListaItemViewModel
{
    public int Id { get; set; }
    public string Nome { get; set; } = string.Empty;
    public int TotalPedidos { get; set; }       // calculado via JOIN/GROUP BY
    public decimal ValorTotalCompras { get; set; }
}

// para a tela de CADASTRO — tem um campo que a entidade nunca teria
public class ClienteCadastroViewModel
{
    [Required(ErrorMessage = "O nome é obrigatório")]
    public string Nome { get; set; } = string.Empty;

    [Required, EmailAddress]
    public string Email { get; set; } = string.Empty;

    [Required, MinLength(6)]
    public string Senha { get; set; } = string.Empty;

    [Compare(nameof(Senha), ErrorMessage = "As senhas não coincidem")]
    public string ConfirmarSenha { get; set; } = string.Empty;
}
Conectando com o Módulo 2

ClienteListaItemViewModel mapeia quase exatamente o resultado da procedure usp_ClienteRelatorioTop que você escreveu no capítulo 7 do Módulo 2 (que já devolvia TotalPedidos e ValorTotal calculados via JOIN/GROUP BY) — o ViewModel é o "molde" C# que recebe exatamente essas colunas.

4. DTO — para trafegar entre camadas/sistemas

DTO (Data Transfer Object) é conceitualmente parecido com um ViewModel — uma classe "só de dados", sem comportamento — mas usado especificamente na fronteira entre sistemas (API ↔ cliente, serviço ↔ serviço), não amarrado a uma tela específica. Você vai usá-lo intensamente no Módulo 4 (APIs).

C# ClienteDto.cs
// exposto por uma API — nunca inclui dados sensíveis/internos da entidade
public class ClienteDto
{
    public int Id { get; set; }
    public string Nome { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    // sem DataCadastro, sem qualquer campo interno — só o que o consumidor da API precisa
}

5. Mapeando entre eles

A conversão entre Model e ViewModel/DTO geralmente é manual (um construtor, um método de mapeamento) ou automatizada por uma biblioteca. Para projetos pequenos/médios, manual costuma ser suficiente e mais fácil de depurar:

C# mapeamento manual
public static class ClienteMapper
{
    public static ClienteDto ParaDto(Cliente cliente) => new()
    {
        Id    = cliente.Id,
        Nome  = cliente.Nome,
        Email = cliente.Email,
    };

    public static Cliente ParaEntidade(ClienteCadastroViewModel viewModel) => new()
    {
        Nome  = viewModel.Nome,
        Email = viewModel.Email,
        Ativo = true,
    };
}
Atenção — por que não expor a entidade direto

Além de carregar campos desnecessários, expor a entidade diretamente numa API ou formulário cria uma vulnerabilidade conhecida como over-posting: se um formulário/JSON malicioso incluir um campo como Ativo=true ou até um Id arbitrário, e o model binding (próximo capítulo) preencher a entidade inteira sem filtro, um usuário pode alterar dados que não deveria. Um ViewModel/DTO com só os campos esperados elimina essa classe de bug.

6. Onde isso te leva

Você já viu [Required], [EmailAddress] e [Compare] nos exemplos acima — o próximo capítulo aprofunda exatamente isso: como o ASP.NET Core preenche esses ViewModels a partir da requisição (model binding) e como valida os dados antes de qualquer regra de negócio rodar.

📌 Resumo do capítulo

  • Model representa o domínio (tabela); ViewModel é moldado para uma tela específica; DTO trafega entre sistemas/camadas.
  • ViewModels podem ter campos calculados (que vêm de procedures agregadas) ou campos "de formulário" que não existem na entidade.
  • Nunca exponha a entidade de domínio diretamente num formulário ou resposta de API — risco de over-posting.
  • Mapeamento manual entre Model e ViewModel/DTO é simples e suficiente para a maioria dos projetos.

✏️ Praticando

  1. Escreva ClienteListaItemViewModel e o mapeamento a partir do resultado de usp_ClienteRelatorioTop (Módulo 2, capítulo 7).
  2. Escreva ClienteCadastroViewModel com os atributos de validação do exemplo, e o método ClienteMapper.ParaEntidade.
  3. Explique, em suas próprias palavras, por que ClienteCadastroViewModel não deveria ter uma propriedade Id editável.