Criando um CRUD com Blazor WASM + Minimal APIs (Parte 1)

Montar um CRUD com Blazor WASM e Minimal APIs é uma das formas mais diretas de praticar desenvolvimento full stack inteiramente em C#. Nesta primeira parte, você cria do zero a solução, os dois projetos e o backend que vai alimentar o frontend.

Saber fazer um CRUD é uma tarefa obrigatória, tanto no dia a dia quanto em processos seletivos. Por isso, vale dominar cada etapa com calma, entendendo o motivo de cada comando.

O que vamos construir no CRUD com Blazor WASM

O projeto completo do CRUD com Blazor WASM terá duas aplicações independentes. De um lado fica uma API construída com Minimal APIs do ASP.NET Core; do outro, uma interface Blazor WebAssembly que roda inteiramente no navegador.

Nesta parte 1, o foco está todo no backend. Em outras palavras, você vai sair daqui com uma API funcional, com banco de dados SQLite e endpoints para listar e cadastrar categorias.

A segunda parte, publicada em Blazor WASM e Minimal APIs: criando um CRUD, parte 2, conecta o frontend a essa API. Lá entram o HttpClient, o serviço de acesso aos dados e a configuração de CORS.

Por que separar frontend e backend

Um app Blazor WebAssembly standalone não depende de um servidor .NET depois que o navegador baixa os arquivos. Segundo a documentação oficial, ele pode ser publicado como um conjunto de arquivos estáticos em qualquer servidor web.

Por outro lado, o navegador não acessa um banco de dados diretamente. Portanto, o frontend precisa de uma API HTTP para ler e gravar informações, e é exatamente esse papel que as Minimal APIs vão cumprir.

Essa separação também traz uma vantagem prática. Como a API fala apenas HTTP e JSON, qualquer outro frontend, em qualquer framework, poderá consumi-la no futuro.

Criando a solução do CRUD com Blazor WASM

Nosso primeiro passo é criar uma pasta para armazenar os projetos, já que teremos mais de um. A forma mais simples é usar o comando mkdir; neste exemplo, a pasta se chama BlzCrd.

mkdir BlzCrd
cd BlzCrd

Em seguida, criamos a solution. Uma solution no .NET funciona como um agrupador de projetos, o que facilita compilar, abrir e organizar tudo de uma vez.

dotnet new sln

Atenção: você precisa executar este comando dentro da pasta que acabou de criar. Com isso, o .NET gera um arquivo com o mesmo nome da pasta e a extensão .sln, que neste caso será BlzCrd.sln.

Ter uma solution não é obrigatório para compilar projetos .NET isolados. Mesmo assim, ela deixa a estrutura mais organizada e permite rodar comandos como dotnet build e dotnet test em todos os projetos com uma única chamada.

Backend com Minimal APIs e frontend com Blazor WASM

Agora vamos criar os projetos que compõem a solução do CRUD com Blazor WASM: um para o backend e outro para o frontend.

# Cria um projeto do tipo Minimal APIs para o backend
dotnet new web -o BlzCrd.Backend

# Cria um projeto do tipo Blazor WebAssembly para o frontend
dotnet new blazorwasm -o BlzCrd.Frontend --pwa

O template web gera o projeto ASP.NET Core vazio, com apenas um endpoint de exemplo. Já o template blazorwasm cria um app Blazor WebAssembly standalone.

A opção --pwa transforma o frontend em uma Progressive Web Application. Na prática, o app ganha um service worker e um manifesto, o que permite instalá-lo no sistema operacional e até usá-lo offline.

Adicionando os projetos à solution

Com os projetos e a solution criados, falta unir as peças. Para isso, adicionamos cada projeto à solution com o comando dotnet sln add.

dotnet sln add ./BlzCrd.Backend
dotnet sln add ./BlzCrd.Frontend

A partir daqui, um dotnet build na raiz compila os dois projetos de uma vez. Além disso, editores como Visual Studio e VS Code passam a enxergar a estrutura completa do CRUD com Blazor WASM.

Ao final desta etapa, a pasta BlzCrd contém o arquivo BlzCrd.sln e duas subpastas, uma para cada projeto. Dessa forma, fica fácil identificar onde mora cada responsabilidade.

Criando a entidade do CRUD com Blazor WASM

Embora não exista uma ordem obrigatória, vamos começar pelo backend e criar a API. O primeiro passo é definir as entidades, e aqui teremos uma única classe para representar uma categoria.

namespace BlzCrd.Backend.Entities;

public class Category
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
}

A classe usa um namespace com escopo de arquivo, o que reduz um nível de indentação. Além disso, as propriedades de texto começam com string.Empty para evitar avisos de referência nula.

Por convenção, o Entity Framework Core trata a propriedade Id como chave primária. Como ela é do tipo int, o banco também vai gerar o valor automaticamente a cada novo registro.

Se você acompanha nosso conteúdo, já deve estar até enjoado de ver criação de entidades. Mesmo assim, ela é a base de todo o CRUD com Blazor WASM que vamos montar.

Criando o DbContext

Com a entidade criada, o próximo passo é criar o DbContext, ou contexto de dados. Para usar o SQLite, precisamos do pacote Microsoft.EntityFrameworkCore.Sqlite.

Navegue até a pasta do projeto backend no terminal e execute os comandos abaixo. Assim, você instala o provedor do SQLite e o pacote de design usado pelas migrations.

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design

Em seguida, criamos o contexto de dados conforme o código abaixo.

using BlzCrd.Backend.Entities;
using Microsoft.EntityFrameworkCore;

namespace BlzCrd.Backend.Data;

public class AppDbContext(DbContextOptions<AppDbContext> options)
    : DbContext(options)
{
    public DbSet<Category> Categories { get; set; } = null!;
}

Repare que a classe usa um primary constructor, recurso do C# 12. Dessa forma, o parâmetro options segue direto para o construtor base do DbContext, sem precisar declarar um construtor completo.

Se quiser entender melhor esse recurso, vale ler o artigo sobre primary constructors no C# 12. Por fim, a propriedade Categories representa a tabela de categorias no banco.

Registrando o DbContext

Com o DbContext criado, precisamos registrá-lo no container de injeção de dependência. Só assim conseguiremos recebê-lo nos endpoints mais adiante.

using BlzCrd.Backend.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Registra o contexto de dados usando um arquivo SQLite local
builder.Services.AddDbContext<AppDbContext>(x =>
{
    x.UseSqlite("DataSource=app.db;Cache=Shared");
});

A string de conexão aponta para um arquivo chamado app.db, que fica na pasta do projeto. Enquanto isso, a opção Cache=Shared permite que várias conexões compartilhem o mesmo cache do SQLite.

O método AddDbContext registra o contexto com tempo de vida scoped. Ou seja, cada requisição HTTP recebe a sua própria instância do AppDbContext.

Migrations no CRUD com Blazor WASM

Com a entidade e o contexto prontos, podemos iniciar o processo de migração. Neste primeiro momento, a migration vai criar o banco e a tabela de categorias do CRUD com Blazor WASM.

Para executar comandos do Entity Framework, precisamos do EF Core Tools. Trata-se de uma ferramenta de linha de comando que estende a CLI do .NET.

# Instalação
dotnet tool install --global dotnet-ef

# Atualização
dotnet tool update --global dotnet-ef

A documentação recomenda usar a versão das ferramentas que corresponde à versão principal dos pacotes do EF Core. Por isso, se a migration falhar logo no início, confira primeiro essa compatibilidade.

Gerando e aplicando a migration

Com o EF Core Tools instalado, agora sim podemos executar a migração.

# Cria uma migration
dotnet ef migrations add v1

# Atualiza o banco de dados
dotnet ef database update

O primeiro comando gera uma pasta Migrations com o código que descreve as alterações no esquema. Em seguida, o segundo comando aplica essas alterações e cria o arquivo app.db.

Vale conhecer uma limitação do provedor SQLite. Segundo a documentação, ele aceita adicionar colunas, mas não suporta bem remover ou alterar colunas sem reconstruir a tabela.

Para um projeto de estudo, isso raramente atrapalha. No entanto, se o seu modelo mudar bastante, apagar o app.db e recriar o banco costuma ser o caminho mais rápido.

Se você quer se aprofundar no assunto, o artigo sobre migrations do EF Core com PostgreSQL mostra o mesmo fluxo em outro banco de dados. Além disso, o balta.io tem uma carreira completa sobre acesso a dados no .NET.

Endpoints do CRUD com Blazor WASM

Com tudo pronto, chegou a hora de criar os endpoints. No caso das Minimal APIs, essa é uma tarefa bem simples, porque cada rota vira uma chamada de método no Program.cs.

app.MapGet("/", () => "Hello World!");

app.MapGet("/v1/categories", async (AppDbContext context) =>
{
    var categories = await context.Categories.AsNoTracking().ToListAsync();
    return Results.Ok(categories);
}).Produces<List<Category>>();

app.MapPost("/v1/categories", async (AppDbContext context, Category category) =>
{
    await context.Categories.AddAsync(category);
    await context.SaveChangesAsync();

    return Results.Created($"/v1/categories/{category.Id}", category);
});

app.Run();

Aqui o framework resolve os parâmetros sozinho. O AppDbContext vem do container de serviços, enquanto o objeto Category vem do corpo JSON da requisição.

Listando categorias com GET

O endpoint GET /v1/categories busca todas as categorias e devolve o resultado com status 200. Para isso, ele usa o método Results.Ok.

O AsNoTracking tem um motivo claro. Como a consulta é somente leitura, o EF Core não precisa montar as informações de rastreamento de alterações, e a consulta fica mais leve.

Já o Produces<List<Category>>() adiciona metadados ao endpoint. Dessa forma, ferramentas de documentação como OpenAPI sabem qual tipo de resposta esperar.

Cadastrando categorias com POST

O endpoint POST /v1/categories recebe uma categoria, adiciona ao contexto e chama SaveChangesAsync. Nesse momento, o EF Core gera o comando INSERT e preenche o Id com o valor criado pelo banco.

Por fim, o Results.Created devolve o status 201 com o cabeçalho Location. Esse cabeçalho aponta para o endereço do novo recurso, como manda a convenção REST.

O tutorial oficial de Minimal APIs no ASP.NET Core segue exatamente esse mesmo padrão. Além disso, ele mostra como evoluir para TypedResults e grupos de rotas com MapGroup.

O Program.cs completo do backend

Juntando todos os trechos, o Program.cs do backend do CRUD com Blazor WASM fica assim.

using BlzCrd.Backend.Data;
using BlzCrd.Backend.Entities;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Registra o contexto de dados usando um arquivo SQLite local
builder.Services.AddDbContext<AppDbContext>(x =>
{
    x.UseSqlite("DataSource=app.db;Cache=Shared");
});

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

// Lista todas as categorias, sem rastreamento de alterações
app.MapGet("/v1/categories", async (AppDbContext context) =>
{
    var categories = await context.Categories.AsNoTracking().ToListAsync();
    return Results.Ok(categories);
}).Produces<List<Category>>();

// Cadastra uma nova categoria
app.MapPost("/v1/categories", async (AppDbContext context, Category category) =>
{
    await context.Categories.AddAsync(category);
    await context.SaveChangesAsync();

    return Results.Created($"/v1/categories/{category.Id}", category);
});

app.Run();

Se quiser revisar os fundamentos dessa abordagem, o artigo sobre Minimal APIs no ASP.NET explica o modelo em detalhes.

Testando a API do CRUD com Blazor WASM

Antes de partir para o frontend, vale confirmar que a API do CRUD com Blazor WASM responde corretamente. Para isso, execute o projeto a partir da pasta do backend.

cd BlzCrd.Backend
dotnet run

O terminal mostra o endereço em que a API está escutando, definido no arquivo Properties/launchSettings.json. Anote essa porta, porque o frontend vai precisar dela na segunda parte.

Enviando requisições de teste

Com a API rodando, envie um POST com um JSON contendo title e description. O exemplo abaixo usa o curl, mas qualquer cliente HTTP serve.

# Substitua a porta pela que aparece no seu terminal
curl -X POST http://localhost:5258/v1/categories \
  -H "Content-Type: application/json" \
  -d '{"title":"Backend","description":"Backend courses"}'

curl http://localhost:5258/v1/categories

O primeiro comando deve devolver o status 201 com a categoria criada, já com o Id preenchido. Em seguida, o segundo comando lista todas as categorias salvas no arquivo app.db.

Se você usa Visual Studio ou VS Code com o C# Dev Kit, também pode testar com um arquivo .http. Assim, as requisições ficam salvas junto do projeto e qualquer pessoa do time consegue repeti-las.

Erros comuns nesta etapa

O erro mais frequente é rodar dotnet ef fora da pasta do backend. Nesse caso, a ferramenta não encontra o projeto nem o AppDbContext e interrompe a execução.

Outro problema comum é esquecer o pacote Microsoft.EntityFrameworkCore.Design. Sem ele, o comando migrations add não consegue gerar o código da migration.

Por fim, se o GET devolver erro dizendo que a tabela não existe, provavelmente o database update não rodou. Então, execute o comando novamente e confira se o arquivo app.db apareceu na pasta.

Assista o vídeo

O vídeo abaixo mostra cada passo desta primeira parte sendo executado na prática.

Próximos passos do CRUD com Blazor WASM

Com a API pronta, o próximo passo do CRUD com Blazor WASM é preparar o frontend para consumi-la. Isso envolve descobrir as portas de cada aplicação no launchSettings.json e registrar um HttpClient apontando para o backend.

Depois disso, entra um serviço que converte JSON em objetos C# e vice-versa. Por fim, a API precisa liberar o CORS, porque frontend e backend rodam em portas diferentes.

Todos esses pontos estão na segunda parte da série. Se quiser conhecer melhor o modelo antes de seguir, o artigo uma visão geral do Blazor apresenta os conceitos principais.

Como criamos o frontend com a opção --pwa, também vale ler o artigo sobre como criar um PWA com Blazor WASM. Assim, você entende o que o service worker faz pelo seu app.

Conclusão

Nesta primeira parte, criamos um backend com ASP.NET Minimal APIs que qualquer frontend pode consumir. Neste caso, porém, ele vai servir de base para o CRUD com Blazor WASM (WebAssembly).

Ao longo do caminho, você criou a solution, os dois projetos, a entidade Category, o AppDbContext com SQLite, a primeira migration e os endpoints de listagem e cadastro.

Fique ligado na segunda parte, que complementa ainda mais o conteúdo visto aqui. E, se quiser aprender Blazor e todo o universo do .NET do zero ao avançado, confira as carreiras do balta.io.

FAQ

Preciso de dois projetos para fazer um CRUD com Blazor WASM?

Sim, no modelo standalone o Blazor WebAssembly roda no navegador e não acessa o banco diretamente. Por isso, uma API separada, como a de Minimal APIs, faz a ponte com os dados.

Para que serve a opção --pwa no template blazorwasm?

Ela adiciona um service worker e um manifesto ao projeto. Assim, o usuário pode instalar o app e usá-lo mesmo com conexão instável.

Por que usar AsNoTracking na listagem?

Porque a consulta é somente leitura e não precisa de rastreamento de alterações. Com isso, o EF Core gasta menos memória e processamento.

Posso trocar o SQLite por outro banco de dados?

Pode, basta instalar o provedor correspondente e trocar o UseSqlite pelo método do novo banco. Depois, gere novas migrations para esse provedor.

Qual a diferença entre Results.Ok e Results.Created?

O Results.Ok devolve o status 200 com o conteúdo pedido. Já o Results.Created devolve o status 201 e o cabeçalho Location com o endereço do recurso criado.