Um Blog Completo com ASP.NET Core

Um Blog Completo com ASP.NET Core

Um site navegável completo em C#: um blog com Razor Pages, com listagem de posts, formulário de criação, página de visualização e persistência em banco de dados. A aula mostra como cada peça se conecta ao curso inteiro — modelo, consultas, injeção de dependência e validação.
Linguagem C#

• • 15 min de leitura

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

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).

Comentários

Mais em Linguagem C#

Polimorfismo: Um Comando, Muitos Comportamentos
Polimorfismo: Um Comando, Muitos Comportamentos

Um mesmo comando produzindo comportamentos diferentes conforme o objeto: o…

Armadilhas Comuns: O Mapa dos Perigos
Armadilhas Comuns: O Mapa dos Perigos

As seis armadilhas que pegam todo programador C#: confundir atribuição com…

Laços de Repetição: while e for
Laços de Repetição: while e for

Os dois laços fundamentais do C#: o while, que repete enquanto uma condição…