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

Nesta segunda parte do guia de CRUD com Blazor WASM e Minimal APIs, vamos conectar o frontend ao backend que criamos no artigo anterior e finalizar as funcionalidades de listagem e cadastro.

Onde paramos no CRUD com Blazor WASM

No artigo anterior, aprendemos a criar um backend e organizar o projeto. Portanto, já temos uma API em execução, com SQLite e endpoints para categorias.

Agora, o foco muda para o lado do cliente. Ou seja, vamos fazer o Blazor WebAssembly conversar com essa API via HTTP.

Para isso, seguiremos alguns passos. Primeiro, descobrimos as URLs de cada aplicação e, em seguida, configuramos o HttpClient.

Depois, criamos um serviço para isolar as chamadas HTTP e usamos esse serviço em uma página. Por fim, liberamos o acesso com CORS.

Todo o exemplo usa .NET 8 e C# 12. Por isso, você verá recursos como construtores primários e expressões de coleção.

Estrutura da solução do CRUD com Blazor WASM

A solução do CRUD com Blazor WASM tem dois projetos independentes. O BlzCrs.Backend hospeda a API com Minimal APIs e o BlzCrs.Frontend contém o app Blazor WebAssembly.

Como são aplicações separadas, cada uma roda em sua própria porta e pode ser publicada em um servidor diferente. Por exemplo, o frontend pode ir para uma hospedagem de arquivos estáticos.

No frontend, vamos criar duas pastas. A pasta Models guarda as classes que representam os dados e a pasta Services concentra o acesso à API.

Essa separação mantém as páginas limpas. Afinal, os componentes Razor só chamam métodos do serviço, sem saber detalhes de URL ou serialização.

URLs dos serviços

O backend usa um arquivo chamado launchSettings.json para determinar a porta em que roda durante o desenvolvimento.

Esse arquivo fica na pasta Properties de cada projeto. Nele, procuramos a seção http, ou a seção https caso você execute a aplicação de outra forma.

Porta da API

O conteúdo do arquivo da API se parece com o trecho abaixo. Repare na propriedade applicationUrl.

// BlzCrs.Backend/Properties/launchSettings.json
"profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5258",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },

Nesse perfil, a porta da API do nosso CRUD com Blazor WASM é a 5258. Anote esse número, porque vamos usá-lo no frontend.

As portas variam de máquina para máquina, já que o template sorteia os valores ao criar o projeto. Então, sempre confira o seu próprio arquivo.

Porta do frontend

Da mesma forma, anote a porta da aplicação que vai rodar o nosso app Blazor WebAssembly.

// BlzCrs.Frontend/Properties/launchSettings.json
"http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
      "applicationUrl": "http://localhost:5293",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },

Aqui, o frontend roda na porta 5293. Além disso, a propriedade inspectUri habilita a depuração do código WebAssembly pelo navegador.

Com as duas portas em mãos, já sabemos para onde o frontend precisa enviar as requisições e qual origem a API precisa liberar.

Configurando o HttpClient no CRUD com Blazor WASM

Nosso próximo passo no CRUD com Blazor WASM é configurar o HttpClient. Ele faz as requisições para a API e converte os resultados de JSON para objetos C#.

No navegador, o HttpClient do Blazor WebAssembly usa a Fetch API por baixo dos panos. Por isso, ele segue as mesmas regras de segurança de qualquer chamada feita por JavaScript.

Classe de configuração

Para começar, criamos duas configurações no frontend. A primeira serve como nome do cliente HTTP e a segunda guarda a URL do backend obtida no passo anterior.

public class Configuration
{
    public static string HttpClientName = "backend";
    public static string BackendUrl = "http://localhost:5258";
}

Centralizar esses valores evita espalhar textos fixos pelo código. Assim, quando a URL mudar, alteramos um único lugar.

Em produção, o ideal é ler a URL de um arquivo de configuração, como o wwwroot/appsettings.json. Para este exemplo, a classe estática atende bem.

Lendo a URL do appsettings.json

Se preferir não fixar a URL no código, o Blazor WebAssembly lê automaticamente o arquivo wwwroot/appsettings.json ao iniciar.

{
  "BackendUrl": "http://localhost:5258"
}

Em seguida, basta ler o valor pelo builder.Configuration no Program.cs do frontend.

// lê a URL do backend e usa um valor padrão caso a chave não exista
var backendUrl = builder.Configuration["BackendUrl"] ?? "http://localhost:5258";

Atenção: o navegador baixa esse arquivo junto com o app. Portanto, nunca coloque segredos, como senhas ou chaves de API, no appsettings.json de um projeto WebAssembly.

Pacote Microsoft.Extensions.Http

Com as configurações criadas, instalamos o pacote Microsoft.Extensions.Http. Ele traz o IHttpClientFactory e funcionalidades adicionais ao pacote HTTP padrão do .NET.

dotnet add package Microsoft.Extensions.Http

De acordo com a documentação sobre chamadas de API no Blazor, esse pacote habilita clientes nomeados e tipados dentro de um app WebAssembly.

Registrando o HttpClient

Com tudo pronto, registramos um HttpClient nomeado no arquivo Program.cs do frontend.

builder.Services.AddHttpClient(
    Configuration.HttpClientName,
    opt => opt.BaseAddress = new Uri(Configuration.BackendUrl));

O primeiro argumento é o nome do cliente. Já o segundo define o endereço base, então as chamadas seguintes só precisam informar o caminho relativo, como v1/categories.

O cliente nomeado é útil quando a aplicação conversa com mais de uma API. Afinal, cada uma pode ter seu próprio nome, endereço e cabeçalhos.

Cliente tipado como alternativa

A documentação também apresenta o cliente tipado. Nesse modelo, o serviço recebe um HttpClient já configurado, sem precisar chamar o CreateClient.

// registra o serviço como cliente tipado, com o endereço base da API
builder.Services.AddHttpClient<CategoryService>(
    client => client.BaseAddress = new Uri(Configuration.BackendUrl));

Com isso, o construtor do serviço passaria a receber um HttpClient diretamente. Ambas as abordagens funcionam, então escolha a que deixar o código mais claro para o seu time.

Neste guia, seguimos com o cliente nomeado, que mantém o mesmo padrão usado no vídeo.

Criando o serviço de categorias

Agora, implementamos o serviço que faz as requisições HTTP para a API. Ele converte os dados de JSON para C# e também de C# para JSON.

Antes disso, o frontend precisa de um modelo com as mesmas propriedades que a API devolve.

namespace BlzCrd.Frontend.Models;

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

Com o modelo pronto, criamos o CategoryService na pasta Services.

using System.Net.Http.Json;
using BlzCrd.Frontend.Models;

namespace BlzCrd.Frontend.Services;

public class CategoryService(IHttpClientFactory httpClientFactory)
{
    public async Task CreateAsync(Category category)
    {
        var client = httpClientFactory.CreateClient(Configuration.HttpClientName);
        await client.PostAsJsonAsync("v1/categories/", category);
    }

    public async Task<List<Category>> GetAsync()
    {
        var client = httpClientFactory.CreateClient(Configuration.HttpClientName);
        return await client.GetFromJsonAsync<List<Category>>("v1/categories") ?? [];
    }
}

A classe usa um construtor primário para receber o IHttpClientFactory. Em seguida, cada método do CRUD com Blazor WASM cria o cliente pelo nome que registramos.

O método PostAsJsonAsync serializa a categoria e envia um POST. Por outro lado, o GetFromJsonAsync faz um GET e já desserializa a lista.

Se a API devolver nulo, a expressão ?? [] retorna uma lista vazia. Dessa forma, a página nunca recebe uma referência nula.

Registrando o serviço no CRUD com Blazor WASM

Para injetar o serviço nas páginas do CRUD com Blazor WASM, também precisamos registrá-lo no container de dependências do Program.cs.

builder.Services.AddScoped<CategoryService>();

No WebAssembly, o escopo Scoped se comporta praticamente como Singleton, já que existe uma única instância do app por aba do navegador.

Criando e listando no CRUD com Blazor WASM

Com o serviço criado, o próximo passo do CRUD com Blazor WASM é usá-lo para listar as categorias e criar uma nova.

Basicamente, vamos usar os métodos GET e POST, sempre passando pelo serviço.

Injetando o serviço

Para usar o serviço na página, primeiro precisamos injetá-lo com a diretiva @inject.

@page "/"
@using BlzCrd.Frontend.Models
@using BlzCrd.Frontend.Services
@inject CategoryService CategoryService

A diretiva @page define a rota da página. Já o @inject pede ao container uma instância do CategoryService e a expõe com o mesmo nome.

Utilizando o serviço

O código da implementação fica bem simples, bastando chamar o serviço injetado.

@code {
    private List<Category> _categories = new();

    private async Task ListAsync()
    {
        _categories = await CategoryService.GetAsync();
    }

    private async Task CreateAsync()
    {
        var category = new Category
        {
            Id = 0,
            Title = "New category",
            Description = "Category description"
        };
        await CategoryService.CreateAsync(category);
    }
}

O método ListAsync busca as categorias e guarda o resultado no campo _categories. Então, o Blazor renderiza a página de novo após o evento.

Já o CreateAsync monta uma categoria de exemplo e envia para a API. O Id vale zero porque o banco gera o valor definitivo.

Montando a interface

Por fim, o HTML fica ainda mais simples. Ele percorre a lista de categorias e oferece um botão para criar e outro para listar.

<PageTitle>Home</PageTitle>

<h1>Categories</h1>

<ul>
    @foreach (var item in _categories)
    {
        <li>@item.Title</li>
    }
</ul>

<button class="btn btn-primary" @onclick="CreateAsync">
    Create
</button>

<button class="btn btn-primary" @onclick="ListAsync">
    List
</button>

O @onclick liga cada botão ao método correspondente. Como os dois métodos retornam Task, o Blazor aguarda a conclusão e atualiza a tela em seguida.

Se você quiser carregar a lista assim que a página abrir, basta chamar o ListAsync dentro do OnInitializedAsync.

@code {
    // carrega as categorias assim que o componente inicializa
    protected override async Task OnInitializedAsync()
    {
        await ListAsync();
    }
}

Também é uma boa ideia exibir um aviso de carregamento enquanto a requisição não termina. Para isso, um campo booleano como _isLoading resolve o problema com poucas linhas.

CORS no CRUD com Blazor WASM

Embora o exemplo pareça pronto, a aplicação ainda vai falhar. Ao executar o CRUD com Blazor WASM, recebemos um erro de CORS, talvez o erro mais famoso do mundo fullstack.

O que é CORS

CORS significa Cross-Origin Resource Sharing. Trata-se de um mecanismo do navegador que bloqueia chamadas entre origens diferentes, a menos que o servidor autorize.

Uma origem combina esquema, domínio e porta. Por isso, http://localhost:5293 e http://localhost:5258 são origens diferentes, mesmo rodando na mesma máquina.

Como o código do CRUD com Blazor WASM executa no navegador, ele está sujeito a essa regra. Em contrapartida, uma chamada feita de servidor para servidor não passa por essa verificação.

Além disso, um POST com corpo JSON dispara uma requisição de preflight do tipo OPTIONS. Nela, o navegador pergunta à API se aceita o método e os cabeçalhos antes de enviar os dados.

Configurando a política na API

Para habilitar o CORS entre o app e a API, fazemos três ajustes no backend. Primeiro, criamos uma classe de configuração.

namespace BlzCrd.Backend;

public static class Configuration
{
    public static string CorsPolicyName = "wasm";
    public static string BackendUrl = "http://localhost:5258";
    public static string FrontendUrl = "http://localhost:5293";
}

Em seguida, configuramos uma política de CORS logo após o registro do SQLite.

// ...
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(x => { x.UseSqlite("DataSource=app.db;Cache=Shared"); });
builder.Services.AddCors(
    options => options.AddPolicy(
        Configuration.CorsPolicyName,
        policy => policy
            .WithOrigins([Configuration.BackendUrl, Configuration.FrontendUrl])
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials()));

var app = builder.Build();
// ...

O WithOrigins lista as origens permitidas. Na prática, a origem que realmente importa aqui é a do frontend, já que é ele quem chama a API pelo navegador.

O AllowAnyMethod e o AllowAnyHeader liberam todos os verbos e cabeçalhos. Já o AllowCredentials permite enviar cookies e credenciais nas requisições.

Vale um cuidado: o ASP.NET Core não aceita combinar AllowAnyOrigin com AllowCredentials. Por isso, listar as origens explicitamente é o caminho correto.

Aplicando a política

Por fim, usamos a política criada para liberar o acesso do frontend à API.

// ...
var app = builder.Build();

app.UseCors(Configuration.CorsPolicyName);

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

O UseCors precisa vir antes do mapeamento dos endpoints. Assim, o middleware adiciona os cabeçalhos corretos em todas as respostas, inclusive no preflight.

A documentação oficial de CORS no ASP.NET Core detalha outras opções, como políticas por endpoint com RequireCors.

Agora, execute a API e o frontend ao mesmo tempo. Ao clicar em Create e depois em List, a nova categoria aparece na tela.

Executando o CRUD com Blazor WASM

Para testar o CRUD com Blazor WASM, os dois projetos precisam rodar juntos. Pelo terminal, abra duas janelas e execute um comando em cada pasta.

# terminal 1: API
cd BlzCrs.Backend
dotnet run

# terminal 2: frontend
cd BlzCrs.Frontend
dotnet run

No Visual Studio, outra opção é configurar múltiplos projetos de inicialização nas propriedades da solução. Assim, um único F5 sobe a API e o app.

Com tudo rodando, abra as ferramentas de desenvolvedor do navegador na aba de rede. Em seguida, clique no botão Create.

Você verá primeiro a requisição OPTIONS do preflight e, logo depois, o POST para v1/categories. Se o CORS estiver correto, as duas respostas chegam sem erro.

Caso o console ainda mostre o bloqueio, confira se a porta em FrontendUrl bate com a do launchSettings.json. Na prática, esse descuido causa a maioria dos erros de CORS no CRUD com Blazor WASM.

Próximos passos para o CRUD com Blazor WASM

O exemplo cobre a criação e a listagem. Para completar o CRUD com Blazor WASM, o mesmo serviço pode ganhar métodos de atualização e exclusão.

O HttpClient oferece o PutAsJsonAsync para atualizar e o DeleteAsync para remover registros. Então, basta seguir o mesmo padrão dos métodos atuais.

// métodos sugeridos para completar o serviço
public async Task UpdateAsync(Category category)
{
    var client = httpClientFactory.CreateClient(Configuration.HttpClientName);
    await client.PutAsJsonAsync($"v1/categories/{category.Id}", category);
}

public async Task DeleteAsync(int id)
{
    var client = httpClientFactory.CreateClient(Configuration.HttpClientName);
    await client.DeleteAsync($"v1/categories/{id}");
}

Esses métodos assumem que a API expõe as rotas de atualização e exclusão com o identificador na URL. Portanto, ajuste os caminhos conforme os endpoints que você criou na primeira parte.

Na interface, cada item da lista pode ganhar botões de editar e excluir, chamando esses métodos e recarregando a lista logo depois.

Também vale tratar o status das respostas. Por exemplo, o método EnsureSuccessStatusCode lança uma exceção quando a API devolve erro.

Por fim, considere trocar a categoria fixa por um formulário com EditForm. Dessa forma, o usuário informa o título e a descrição na própria tela.

Conclusão do CRUD com Blazor WASM

CRUD é uma tarefa requisitada tanto em processos seletivos quanto no dia a dia das empresas. Nunca se sabe quando a necessidade vai surgir, então é bom estar preparado.

Com este artigo, você tem a base de um CRUD com Blazor WASM e Minimal APIs, do HttpClient nomeado até a política de CORS.

Ficou faltando alguma coisa? Me conte o que mais você gostaria de ver aqui no blog.

Aproveite também para assistir ao vídeo abaixo.

Assista ao vídeo

O vídeo mostra todo o passo a passo deste artigo, do backend ao frontend.

FAQ

Por que recebo erro de CORS ao chamar a API pelo Blazor WebAssembly?

O app e a API rodam em portas diferentes, então o navegador os trata como origens distintas. A API precisa liberar a origem do frontend com uma política de CORS.

Preciso instalar o Microsoft.Extensions.Http no Blazor WebAssembly?

Sim, quando você quer usar o IHttpClientFactory com clientes nomeados ou tipados. Sem ele, só resta registrar uma instância simples de HttpClient.

Posso usar AllowAnyOrigin junto com AllowCredentials?

Não, o ASP.NET Core rejeita essa combinação por segurança. Liste as origens com WithOrigins quando precisar de credenciais.

Qual a diferença entre GetFromJsonAsync e PostAsJsonAsync?

O GetFromJsonAsync faz um GET e desserializa o JSON da resposta. Já o PostAsJsonAsync serializa um objeto e o envia no corpo de um POST.

Onde fica a primeira parte deste guia?

A primeira parte mostra como criar o backend com Minimal APIs e organizar o projeto. O link está logo no início deste artigo.