CRUD completo com Razor Pages: agenda de contatos com cadastro, edição, exclusão e visualização de detalhes, construída com Repository Pattern, Entity Framework Core e PostgreSQL — demonstrando o padrão Razor Pages como alternativa ao MVC para aplicações web orientadas a página.
Desenvolvedores .NET que aprendem ASP.NET Core via MVC tradicional frequentemente têm dificuldade para construir aplicações web orientadas a fluxos de página — onde cada URL corresponde a uma ação específica do usuário, como "criar contato", "editar contato" ou "confirmar exclusão". O padrão MVC foi projetado para APIs e aplicações com separação estrita de responsabilidades, mas pode ser excessivo quando o objetivo é uma aplicação web simples com interações diretas de formulário.
O problema concreto que este projeto endereça é duplo:
- Acoplamento desnecessário entre controller e view em operações de página única: no MVC, uma operação de CRUD de contatos exige um controller com múltiplos action methods, rotas configuradas manualmente e views separadas. Para cada página existe uma rota, um método, uma view — a navegação entre eles não é explícita no código.
- Falta de coesão entre lógica de página e marcação: Razor Pages coloca o
PageModele o.cshtmlno mesmo diretório, com a mesma convenção de nome. Quem abreEdit.cshtmlencontraEdit.cshtml.csimediatamente ao lado. Esse alinhamento reduz o custo cognitivo de navegar pelo projeto.
A questão central: como construir uma aplicação web .NET com CRUD completo onde cada página seja autocontida — com sua lógica, validação e persistência — sem o overhead de um controller centralizado?
Este projeto foi desenvolvido no contexto do estudo de Razor Pages como alternativa ao padrão MVC para aplicações web orientadas a formulário. O domínio — uma agenda de contatos com nome, telefone, email, data de nascimento e foto — foi escolhido por ser simples o suficiente para não obscurecer o padrão arquitetural, mas completo o suficiente para demonstrar todas as operações de CRUD com validação real.
A escolha do PostgreSQL como banco de dados reflete o ecossistema mais comum em aplicações .NET modernas que precisam de banco relacional open source — sem custo de licença, com suporte de primeira classe no EF Core via provider Npgsql, e compatível com ambientes cloud como Azure Database for PostgreSQL e Render.
O Repository Pattern foi adicionado deliberadamente para demonstrar que Razor Pages não implica abandono de boas práticas de separação de responsabilidades. O PageModel não acessa o DbContext diretamente — ele depende da interface IContatoRepository, o que mantém a testabilidade da lógica de página independente da implementação de persistência.
Para estruturar este projeto, foram adotadas as seguintes premissas:
- Cada página Razor é autocontida: a lógica de negócio correspondente à página fica no
PageModelda mesma pasta, não em um controller centralizado. - A validação de dados é dupla: atributos de DataAnnotations no modelo (
[Required],[EmailAddress],[StringLength]) garantem validação server-side;asp-validation-summarye tag helpers expõem os erros ao usuário sem JavaScript adicional. - O repositório isola completamente o acesso ao banco de dados. Os
PageModelsdependem deIContatoRepository, não deAppDbContext. Isso permite substituir a implementação em testes sem modificar as páginas. - O campo
Fotoarmazena uma URL de imagem, não um arquivo binário. Isso simplifica o armazenamento inicial e deixa a evolução para upload real (Azure Blob Storage, por exemplo) como próximo passo natural. - Connection strings nunca são versionadas com credenciais reais. O
appsettings.jsoncontém apenas credenciais de desenvolvimento local; em produção, variáveis de ambiente sobrescrevem os valores.
Razor Pages como unidade de organização
Em vez de agrupar lógica por tipo (Controllers/, Views/), Razor Pages agrupa por funcionalidade: Pages/Contatos/ contém todas as páginas e seus respectivos PageModels para o domínio de contatos. Cada arquivo .cshtml tem seu .cshtml.cs ao lado — a correspondência é explícita pela convenção de nome, não por roteamento configurado separadamente.
As rotas são definidas diretamente nas páginas via @page "{id:int}", com constraint de tipo que rejeita IDs inválidos antes de chegar ao OnGetAsync. Isso elimina uma classe inteira de bugs onde IDs inválidos chegam ao repositório.
Repository Pattern com interface
O IContatoRepository define cinco operações assíncronas: GetAllAsync, GetByIdAsync, AddAsync, UpdateAsync e DeleteAsync. O ContatoRepository implementa a interface usando o AppDbContext com EF Core. O Program.cs registra IContatoRepository com AddScoped<IContatoRepository, ContatoRepository>(), garantindo que cada request HTTP receba uma instância isolada do repositório e do contexto.
Fluxo de dados em cada operação CRUD
OnGetAsync carrega dados do repositório e os expõe via propriedades do PageModel para a view. OnPostAsync recebe dados via [BindProperty], valida com ModelState.IsValid e persiste via repositório. Em caso de erro de validação, a página é reexibida com os erros inline. Em caso de sucesso, o redirecionamento para Index evita resubmissão do formulário no reload.
EF Core com Code-First e migrations
O AppDbContext expõe apenas DbSet<Contato>. As migrations são geradas pelo EF Core CLI a partir do modelo C# — o banco PostgreSQL é criado e versionado pelo código, não por scripts SQL manuais. O comando dotnet ef database update aplica as migrations e cria o banco AgendaDb automaticamente.
A escolha foi deliberada para aprender a diferença entre os dois padrões. MVC faz mais sentido quando múltiplas views compartilham a mesma lógica de controller ou quando a separação entre modelo, view e controller precisa ser explícita (APIs, por exemplo). Razor Pages faz mais sentido quando cada URL corresponde a uma ação específica do usuário e a lógica é coesa com a página. Para uma agenda de contatos, cada operação é uma página distinta — Razor Pages reflete essa realidade melhor do que MVC.
O PageModel poderia acessar o AppDbContext diretamente — é mais simples e o EF Core é testável com bancos em memória. A decisão de adicionar o repositório foi educacional: demonstrar que Razor Pages não implica abandono de padrões de design. O benefício prático aparece quando a implementação de persistência muda — trocar PostgreSQL por SQL Server, por exemplo, exige mudanças apenas no ContatoRepository e no provider, não nas páginas.
Na primeira versão, o OnPostAsync do EditModel não redirecionava após salvar — apenas retornava Page(). O resultado: ao recarregar a página após edição, o browser resubmetia o formulário e duplicava a operação de update. A solução foi o padrão Post-Redirect-Get: OnPostAsync salva e redireciona para Index, que é um GET. O reload do browser refaz o GET, não o POST. Esse padrão é fundamental em qualquer aplicação web com formulários.
Substituiria o campo Foto de URL de string por upload real com IFormFile, armazenando os arquivos em wwwroot/imagens/ com nome gerado por GUID para evitar colisões. Também adicionaria paginação no Index para não carregar todos os contatos de uma vez — o GetAllAsync atual não tem limite, o que se torna problema com volumes maiores.
O projeto entregou uma aplicação web funcional com os seguintes resultados concretos:
CRUD completo operacional: 5 páginas Razor (Index, Create, Edit, Delete, Details) com seus respectivos PageModels, cobrindo todas as operações de gestão de contatos com validação server-side e feedback de erro inline.
Validação robusta no modelo: o Contato tem 5 campos validados com DataAnnotations — [Required], [StringLength], [EmailAddress] e [DataType]. Entradas inválidas são rejeitadas antes de chegar ao repositório, com mensagens de erro exibidas via asp-validation-summary.
Repository Pattern testável: IContatoRepository com 5 métodos assíncronos, implementado por ContatoRepository via EF Core. A interface permite substituir a implementação sem modificar as páginas.
Banco PostgreSQL gerenciado por migrations: o AppDbContext com DbSet<Contato> e as migrations do EF Core criam e versionam o banco AgendaDb automaticamente. Qualquer clone do repositório com PostgreSQL disponível pode rodar a aplicação com dois comandos.
Armazenamento de fotos por URL: o campo Foto aceita URLs de imagem externas, exibidas na página de detalhes via tag <img>. Simples, funcional, e com caminho claro para evolução a upload local.
- Implementar upload real de imagem com
IFormFile, armazenando arquivos emwwwroot/imagens/com nome gerado por GUID. - Adicionar paginação no
Indexcom parâmetrospageepageSize, evitando carregamento irrestrito de contatos. - Implementar busca por nome no
Indexcom filtro no repositório (GetByNameAsync). - Adicionar autenticação com ASP.NET Core Identity para proteger as operações de criação, edição e exclusão.
- Substituir DataAnnotations por FluentValidation para validações mais complexas (formato de telefone brasileiro, por exemplo).
- Adicionar testes unitários dos
PageModelscom mocks doIContatoRepository.
- .NET SDK 8.0
- PostgreSQL 14+ em execução local
- Git
# 1. Clone o repositório
git clone https://github.com/Santosdevbjj/razor-Pages-Projeto.git
cd razor-Pages-Projeto
# 2. Configure a connection string em appsettings.json
# "PostgresConnection": "Host=localhost;Database=AgendaDb;Username=postgres;Password=SUA_SENHA"
# 3. Instale a CLI do EF Core (se necessário)
dotnet tool install --global dotnet-ef
# 4. Crie o banco e aplique as migrations
dotnet ef migrations add InitialCreate
dotnet ef database update
# 5. Execute a aplicação
dotnet run
# Acesse em: http://localhost:5000razor-Pages-Projeto/
├── Program.cs # DI: DbContext, Repository, Razor Pages
├── appsettings.json # Connection string PostgreSQL
├── AgendaContatos.csproj # EF Core, Npgsql
│
├── Models/
│ └── Contato.cs # Id, Nome, Telefone, Email, DataNascimento, Foto
│
├── Data/
│ └── AppDbContext.cs # DbSet<Contato>
│
├── Services/
│ ├── IContatoRepository.cs # Contrato: 5 operações assíncronas
│ └── ContatoRepository.cs # Implementação via EF Core
│
├── Pages/
│ ├── Index.cshtml # Página inicial
│ └── Contatos/
│ ├── Index.cshtml # Lista de contatos com ações
│ ├── Index.cshtml.cs # OnGetAsync → carrega lista
│ ├── Create.cshtml # Formulário de criação
│ ├── Create.cshtml.cs # OnPostAsync → valida e salva
│ ├── Edit.cshtml # Formulário de edição
│ ├── Edit.cshtml.cs # OnGetAsync + OnPostAsync
│ ├── Delete.cshtml # Confirmação de exclusão
│ ├── Delete.cshtml.cs # OnGetAsync + OnPostAsync
│ ├── Details.cshtml # Visualização completa
│ └── Details.cshtml.cs # OnGetAsync → carrega por ID
│
└── wwwroot/
└── imagens/ # Diretório para imagens locais
Usuário acessa /Contatos
│
▼
Index.cshtml ←─── OnGetAsync() → IContatoRepository.GetAllAsync()
│
├── [Novo Contato] → /Contatos/Create
│ └── OnPostAsync() → AddAsync() → RedirectToPage("Index")
│
├── [Editar] → /Contatos/Edit/{id}
│ ├── OnGetAsync(id) → GetByIdAsync(id)
│ └── OnPostAsync() → UpdateAsync() → RedirectToPage("Index")
│
├── [Detalhes] → /Contatos/Details/{id}
│ └── OnGetAsync(id) → GetByIdAsync(id)
│
└── [Excluir] → /Contatos/Delete/{id}
├── OnGetAsync(id) → GetByIdAsync(id) [confirmação]
└── OnPostAsync(id) → DeleteAsync(id) → RedirectToPage("Index")
| Camada | Tecnologia | Justificativa |
|---|---|---|
| Runtime | .NET 8, C# 12 | LTS, nullable safety, records |
| UI | Razor Pages | Coesão página-lógica, convenção sobre configuração |
| ORM | EF Core 8 + Npgsql | Code-first, migrations automáticas, PostgreSQL nativo |
| Banco | PostgreSQL 14+ | Open source, sem licença, padrão em cloud |
| Validação | DataAnnotations + Tag Helpers | Server-side com feedback inline sem JavaScript |
| DI | ASP.NET Core built-in | AddScoped para DbContext e repositório |
Sergio Santos
Data Engineer & Cloud Architect — 15+ anos em sistemas críticos bancários (Bradesco)
Campus Expert DIO · Bootcamp GFT Start #7 .NET