Até aqui, nossa incursão na web produziu APIs — programas que servem dados em JSON para outros programas consumirem. Mas quando você digita um endereço no navegador e vê uma página com texto, formulários e botões, o que o servidor entrega não é JSON: é HTML, a linguagem das páginas web. Esta aula dá esse salto final, e é o projeto que coroa a fase web: vamos construir um blog completo — um site de verdade, com páginas que uma pessoa acessa e navega, um formulário para escrever posts, e persistência num banco de dados. Usaremos o Razor Pages, o modelo do ASP.NET Core feito sob medida para sites centrados em conteúdo, como um blog. É o capstone da web: aqui, o C# que você dominou gera as próprias páginas que o usuário final vê, fechando o ciclo do backend à interface. Leia com calma; construiremos o site inteiro, peça por peça, e você reconhecerá, em cada uma, algo que já sabe.
O que é o Razor Pages
O ASP.NET Core organiza um site em Razor Pages em torno de páginas: cada página do blog (a lista de posts, o formulário de novo post, a visualização de um post) é um par de arquivos — um .cshtml (o HTML com C# embutido, chamado Razor) e um .cshtml.cs (o código que alimenta a página). Essa organização "uma página, dois arquivos" é intuitiva e casa perfeitamente com sites de conteúdo como blogs.
O Razor é a sintaxe que mistura HTML e C#: dentro do HTML, o símbolo @ introduz código C#. Onde num texto comum você teria conteúdo fixo, no Razor você pode escrever @post.Titulo e o valor da property aparece ali, ou usar @foreach para repetir um bloco de HTML para cada item de uma lista. É C# gerando HTML — a ponte entre a lógica que você domina e a página que o usuário vê no navegador. Se as APIs transformavam objetos em JSON, o Razor transforma objetos em páginas.
Passo 1: o projeto e o modelo
Criamos o projeto Razor Pages e adicionamos o EF Core (usaremos PostgreSQL, mas qualquer banco da fase serve):
dotnet new webapp -n MeuBlog # modelo de Razor Pages
cd MeuBlog
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
O modelo é o post do blog — uma classe comum, com validação declarativa (as anotações da aula anterior):
using System.ComponentModel.DataAnnotations;
class Post
{
public int Id { get; set; }
[Required(ErrorMessage = "O título é obrigatório.")]
[StringLength(200)]
public string Titulo { get; set; } = "";
[Required(ErrorMessage = "O conteúdo é obrigatório.")]
public string Conteudo { get; set; } = "";
public DateTime PublicadoEm { get; set; } = DateTime.Now;
}
E o contexto do EF Core, que exporá a tabela de posts:
using Microsoft.EntityFrameworkCore;
class BlogContext : DbContext
{
public BlogContext(DbContextOptions<BlogContext> opcoes) : base(opcoes) { }
public DbSet<Post> Posts => Set<Post>();
}
O contexto recebe suas opções pelo construtor — pronto para a injeção de dependência da aula anterior. Registramos o contexto e habilitamos o Razor Pages no Program.cs:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages(); // habilita o Razor Pages
builder.Services.AddDbContext<BlogContext>(o => // registra o contexto (injeção)
o.UseNpgsql("Host=localhost;Database=blog_db;Username=curso;Password=senha123"));
var app = builder.Build();
app.UseStaticFiles(); // serve arquivos como CSS e imagens
app.MapRazorPages(); // liga as páginas Razor aos endereços
app.Run();
Depois, criamos o banco blog_db (com o psql, como nas aulas de banco) e aplicamos as migrações (dotnet ef migrations add Inicial e dotnet ef database update), criando a tabela de posts. A infraestrutura está pronta; agora, as páginas.
Passo 2: a página de listagem
A página inicial lista todos os posts. Ela é o par Index.cshtml (a página) + Index.cshtml.cs (o código). Comecemos pelo código, que busca os posts do banco:
// Arquivo: Pages/Index.cshtml.cs
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
public class IndexModel : PageModel
{
private readonly BlogContext _db;
// O BlogContext é INJETADO pelo construtor (injeção de dependência):
public IndexModel(BlogContext db) => _db = db;
// Property que a página vai exibir:
public List<Post> Posts { get; set; } = new();
// OnGet roda quando a página é acessada (o carregamento normal):
public async Task OnGetAsync()
{
// Busca os posts, os mais recentes primeiro (LINQ + assincronia):
Posts = await _db.Posts.OrderByDescending(p => p.PublicadoEm).ToListAsync();
}
}
Reconheça tudo o que já sabe: o BlogContext chega por injeção de dependência (aula anterior), a consulta usa LINQ (OrderByDescending) de forma assíncrona (ToListAsync). O método OnGetAsync é chamado automaticamente quando alguém acessa a página, e preenche a property Posts, que a página exibirá. Agora a página (o HTML com Razor):
@* Arquivo: Pages/Index.cshtml *@
@page
@model IndexModel
<h1>Meu Blog</h1>
<a href="/Criar">Escrever novo post</a>
@* Se não houver posts, avisa; senão, lista cada um: *@
@if (!Model.Posts.Any())
{
<p>Ainda não há posts.</p>
}
else
{
@foreach (var post in Model.Posts) @* C# gerando HTML: repete o bloco por post *@
{
<article>
<h2><a href="/Post/@post.Id">@post.Titulo</a></h2>
<p><small>Publicado em @post.PublicadoEm.ToString("dd/MM/yyyy")</small></p>
</article>
}
}
Aqui você vê o Razor em ação. O @model IndexModel liga a página ao seu código, dando acesso a Model.Posts. O @if e o @foreach são C# controlando a geração de HTML — se a lista está vazia, mostra um aviso; senão, gera um bloco <article> para cada post, inserindo @post.Titulo e a data formatada. É a lógica que você domina (condicionais, laços, LINQ) tecendo a página. Cada título é um link para a página daquele post (/Post/@post.Id), que criaremos adiante.
Passo 3: a página de criação (com formulário)
Agora o coração interativo: a página que permite escrever um post. O código trata dois momentos — exibir o formulário (quando a página é acessada) e receber os dados enviados (quando o formulário é submetido):
// Arquivo: Pages/Criar.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class CriarModel : PageModel
{
private readonly BlogContext _db;
public CriarModel(BlogContext db) => _db = db;
// [BindProperty] liga esta property aos campos do formulário enviado:
[BindProperty]
public Post Post { get; set; } = new();
// Ao ACESSAR a página: apenas exibe o formulário vazio.
public void OnGet() { }
// Ao ENVIAR o formulário:
public async Task<IActionResult> OnPostAsync()
{
// Verifica a validação (as anotações do modelo, da aula anterior):
if (!ModelState.IsValid)
return Page(); // reexibe o formulário com as mensagens de erro
_db.Posts.Add(Post);
await _db.SaveChangesAsync(); // persiste no banco
return RedirectToPage("Index"); // volta à listagem após criar
}
}
Três novidades importantes. O [BindProperty] conecta a property Post aos campos do formulário: quando o usuário envia o formulário, o ASP.NET Core preenche Post com os valores digitados, automaticamente. O ModelState.IsValid verifica as regras de validação declaradas no modelo (os [Required] da aula anterior) — se o título estiver vazio, IsValid é falso, e reexibimos o formulário com os erros, sem salvar (o "valide antes de agir" da aula passada, agora numa página web). E o RedirectToPage leva o usuário de volta à listagem após criar o post. A página com o formulário:
@* Arquivo: Pages/Criar.cshtml *@
@page
@model CriarModel
<h1>Novo Post</h1>
@* O formulário: method="post" envia para o OnPostAsync. *@
<form method="post">
<div>
<label>Título:</label>
<input asp-for="Post.Titulo" />
@* Mostra a mensagem de validação deste campo, se houver: *@
<span asp-validation-for="Post.Titulo"></span>
</div>
<div>
<label>Conteúdo:</label>
<textarea asp-for="Post.Conteudo"></textarea>
<span asp-validation-for="Post.Conteudo"></span>
</div>
<button type="submit">Publicar</button>
</form>
Os atributos asp-for e asp-validation-for são auxiliares do Razor que ligam os campos do formulário às properties do modelo e exibem as mensagens de validação automaticamente. O asp-for="Post.Titulo" gera um campo de entrada ligado à property Post.Titulo; quando o formulário é enviado, o [BindProperty] do código recebe o valor. E o asp-validation-for mostra a mensagem de erro daquele campo (o "O título é obrigatório." do modelo) caso a validação falhe. Formulário, validação e persistência, costurados com o que você já sabia.
Passo 4: a página de visualização de um post
Falta a página que mostra um post individual, acessada pelos links da listagem (/Post/5). Ela recebe o id pelo endereço. O código:
// Arquivo: Pages/Post.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class PostModel : PageModel
{
private readonly BlogContext _db;
public PostModel(BlogContext db) => _db = db;
public Post? Post { get; set; } // pode não achar (tratamento de nulo)
// Recebe o 'id' do endereço e busca o post correspondente:
public async Task<IActionResult> OnGetAsync(int id)
{
Post = await _db.Posts.FindAsync(id);
if (Post == null) return NotFound(); // 404 se não existe
return Page();
}
}
O parâmetro id chega do endereço, o FindAsync busca o post, e — reencontrando o tratamento de nulo da fase de robustez e o 404 da API — se não existir, retornamos NotFound(). A página:
@* Arquivo: Pages/Post.cshtml *@
@page "{id:int}"
@model PostModel
<article>
<h1>@Model.Post!.Titulo</h1>
<p><small>Publicado em @Model.Post.PublicadoEm.ToString("dd/MM/yyyy")</small></p>
<div>@Model.Post.Conteudo</div>
</article>
<a href="/">← Voltar</a>
O @page "{id:int}" no topo declara que esta página recebe um id inteiro pelo endereço — é o que faz /Post/5 chegar com id = 5 ao OnGetAsync. O resto exibe o post com Razor. Com esta página, o blog está completo e navegável: você lista os posts, clica num para lê-lo, e escreve novos por um formulário — tudo persistido no banco.
Passo 5: rodando o blog
Com dotnet run, o site sobe. Acesse o endereço indicado no navegador e você verá a página inicial do blog. Clique em "Escrever novo post", preencha o formulário, publique — e o post aparece na listagem, tendo sido salvo no banco. Reinicie a aplicação: os posts continuam lá, porque vivem no banco de dados. Você construiu um site web de verdade, do zero, em C#. Aquele Console.WriteLine da primeira aula do curso agora é um blog que gera páginas HTML e as serve pela web para quem o acessar.
Onde este blog se conecta ao curso inteiro
Vale a retrospectiva, porque este projeto é uma síntese tão rica quanto o capstone do núcleo. As classes e anotações de validação vêm da fase de objetos e da aula anterior. As consultas LINQ que buscam e ordenam posts são a fase de coleções. O tratamento de nulo (Post?, o 404) é a fase de robustez. O async/await em cada acesso ao banco é a fase de concorrência. A injeção de dependência que entrega o BlogContext às páginas é a aula da API com banco. A validação na fronteira (ModelState.IsValid) é a aula anterior. E o EF Core com migrações é a fase de bancos. Um blog não foi um assunto novo e monolítico; foi a orquestração de tudo o que você já sabia, com a fina camada do Razor por cima para gerar HTML. Essa é a lição maior da fase: dominada a linguagem e seus padrões, cada novo tipo de aplicação — uma API, um site, e a seguir jogos — é principalmente uma questão de empacotar o que você conhece de um jeito diferente.
Um site navegável de verdade ficou de pé. Com Razor Pages, páginas HTML passaram a ser geradas a partir de C#, com listagem, formulário de criação e visualização individual, tudo apoiado num banco de dados — um blog completo, do formulário ao post publicado.
O ponto que merece destaque é onde este projeto se conectou ao curso inteiro: as classes do modelo, as consultas para buscar e ordenar, a injeção de dependência, a validação e as migrações do banco. Praticamente nada aqui foi conhecimento novo; a camada específica de web é fina, e o que sustenta a aplicação é o C# construído desde a primeira aula.
Fontes e leituras recomendadas
- Introdução ao Razor Pages — a documentação oficial do modelo de páginas usado neste blog; a base desta aula.
- Razor Pages com EF Core (tutorial) — o tutorial completo de um site Razor Pages com banco de dados, muito próximo do nosso.
- A sintaxe Razor — o
@,@foreach,@ife o restante da sintaxe que mistura C# e HTML. - Auxiliares de formulário (Tag Helpers) — os
asp-foreasp-validation-forque ligam formulários ao modelo. - Ligação de dados de formulário — como o
[BindProperty]preenche as properties com os dados enviados.
Exercícios
Exercício 1
Monte o projeto MeuBlog completo: crie o modelo Post, o BlogContext, configure o Program.cs, o banco blog_db e as migrações. Crie as três páginas (Index, Criar, Post) e rode com dotnet run. Confirme que consegue criar um post pelo formulário e vê-lo na listagem e na página individual.
Ver resposta
✓ Resposta: A montagem segue os passos da aula: modelo, contexto, Program.cs com AddRazorPages e AddDbContext, criação do banco e migrações, e as três páginas nos arquivos Pages/Index, Pages/Criar e Pages/Post (cada um com seu .cshtml e .cshtml.cs). Com dotnet run, a página inicial aparece; ao criar um post pelo formulário, ele é salvo e passa a constar na listagem e acessível por seu link individual — confirmando o ciclo completo de um site com banco.
Exercício 2
Teste a validação: na página de criação, envie o formulário com o título vazio. Confirme que a página é reexibida com a mensagem de erro e que nenhum post é criado. Explique qual mecanismo (da aula anterior) fez isso funcionar e onde ele aparece no código da página.
Ver resposta
✓ Resposta: Enviando o formulário com título vazio, a página de criação é reexibida com a mensagem "O título é obrigatório." e nenhum post é salvo. O mecanismo é a validação declarativa da aula anterior: as anotações ([Required]) no modelo Post são verificadas por ModelState.IsValid dentro do OnPostAsync; como o título vazio torna IsValid falso, o código executa return Page() (reexibe o formulário com os erros) antes de chegar ao SaveChangesAsync, protegendo o banco de dados inválidos. O trecho aparece no início do OnPostAsync da página de criação.
Exercício 3
Adicione ao blog uma funcionalidade de exclusão: um botão "Excluir" na página de um post que, ao ser acionado, remova o post do banco e redirecione à listagem. (Dica: você precisará de um método OnPost que receba o id, faça Remove e SaveChangesAsync.)
Ver resposta
✓ Resposta: Uma solução adiciona ao PostModel um método:
public async Task<IActionResult> OnPostExcluirAsync(int id)
{
var post = await _db.Posts.FindAsync(id);
if (post != null)
{
_db.Posts.Remove(post);
await _db.SaveChangesAsync();
}
return RedirectToPage("Index");
}
E, na página, um pequeno formulário com um botão que envia para esse método (usando asp-page-handler="Excluir" e passando o id). O padrão é o mesmo do envio de criação: receber a ação, alterar o banco com Remove + SaveChangesAsync, e redirecionar à listagem.
Exercício 4
Identifique, no código do blog, um uso concreto de cada uma destas partes do curso e explique-o em uma frase: LINQ, async/await, injeção de dependência e tratamento de nulo. Aponte o arquivo e o trecho aproximado.
Ver resposta
✓ Resposta: Exemplos concretos: LINQ — em Index.cshtml.cs, _db.Posts.OrderByDescending(p => p.PublicadoEm) ordena os posts do mais recente ao mais antigo. async/await — o await _db.Posts...ToListAsync() no OnGetAsync, que acessa o banco sem travar o programa. Injeção de dependência — o construtor public IndexModel(BlogContext db), que recebe o contexto pronto do sistema de injeção. Tratamento de nulo — em Post.cshtml.cs, public Post? Post { get; set; } e o if (Post == null) return NotFound();, que tratam o caso de o post não existir.
Exercício 5
Sem código: explique a diferença entre o que uma API REST (das aulas anteriores) entrega ao cliente e o que uma aplicação Razor Pages entrega. Por que um blog é melhor servido por Razor Pages, e um backend para um aplicativo de celular é melhor servido por uma API?
Ver resposta
✓ Resposta: Uma API REST entrega dados em formato JSON — texto estruturado para outro programa consumir e decidir o que fazer com ele. Uma aplicação Razor Pages entrega HTML — páginas prontas para um humano ver e navegar no navegador. Um blog é melhor servido por Razor Pages porque seu consumidor final é uma pessoa que quer ler páginas formatadas diretamente no navegador, então gerar o HTML no servidor é o caminho natural. Um backend para um aplicativo de celular é melhor servido por uma API porque quem consome os dados é o aplicativo, que tem sua própria interface nativa e só precisa dos dados crus (JSON) para exibi-los à sua maneira — entregar HTML a ele seria inútil. A escolha depende de quem consome: um humano vendo páginas (Razor Pages) ou um programa consumindo dados (API).