Introdução
No artigo anterior desta série, analisei se Blazor WebAssembly está pronto para produção corporativa comparando-o com Angular. A conclusão foi nuançada — Blazor WASM é viável para cenários corporativos internos, mas tem trade-offs que precisam ser avaliados caso a caso. Hoje vou provar na prática construindo um CRUD completo de produtos com DataGrid paginado, formulários com validação, dialogs, notificações e inline editing — tudo em C#, sem escrever uma linha de JavaScript.
O componente de UI que escolhi é o Radzen Blazor, uma biblioteca open source (licença MIT) com mais de 70 componentes gratuitos. A razão principal: Radzen entrega uma experiência visual madura para cenários CRUD corporativos, com DataGrid, formulários, validação, dialogs e notificações prontos para uso. O JavaScript que roda internamente (Radzen.Blazor.js) é da própria biblioteca — o desenvolvedor nunca toca em JS diretamente.
O que vou construir neste tutorial:
- API REST com Minimal API para Produtos e Categorias (10 endpoints)
- Blazor WASM Standalone consumindo a API via
HttpClient - DataGrid paginado com busca, ordering e ações por linha
- Formulário em dialog reutilizável para criação e edição
- Inline editing para Categorias (pattern alternativo ao dialog)
- Exclusão com confirmação e notificações visuais
Todo o código está no repositório blog-zocateli-sample no GitHub. A ideia é que você clone, rode e forme sua própria opinião. Este artigo é o 2º da série “Frontend Moderno” — meu objetivo é demonstrar que o ecossistema de componentes Blazor já suporta cenários reais de produção, com uma experiência de desenvolvimento familiar para quem vem do .NET.
ℹ️ Informação: Radzen Blazor é open source (MIT) e inclui 70+ componentes free. O
Radzen.Blazor.jsé JavaScript interno da biblioteca — o desenvolvedor nunca escreve JavaScript diretamente. A versão usada neste tutorial é a 10.2.0 com .NET 10.
Pré-requisitos
Para acompanhar este tutorial, você vai precisar de:
- .NET 10 SDK (10.0.201 ou superior) — download oficial
- IDE: VS Code com extensão C# Dev Kit, ou Visual Studio 2022 17.14+
- Conhecimento básico de C# e REST APIs
- Terminal (PowerShell, bash ou zsh)
Clone o repositório com todo o código pronto:
| |
Verifique se o SDK está instalado:
| |
Output esperado:
| |
💡 Dica: Se você usa o VS Code com Dev Containers, o
.devcontainer/do repositório já tem o .NET 10 SDK configurado. Basta abrir o projeto no container e tudo estará pronto.
Criando o Projeto Blazor WASM Standalone
O template blazorwasm do .NET cria uma aplicação Blazor WebAssembly Standalone — uma SPA que roda inteiramente no browser via WebAssembly, sem servidor ASP.NET Core hospedando. Diferente do modelo Hosted (que inclui um projeto Server), o Standalone é uma SPA pura que consome APIs externas via HTTP, exatamente como uma aplicação Angular ou React.
Scaffolding do projeto
| |
O primeiro comando cria o projeto, o segundo adiciona à solution e o terceiro instala o Radzen Blazor — a biblioteca de componentes UI. O .csproj resultante fica enxuto:
| |
Configurando Program.cs
O Program.cs é o entry point da SPA. Aqui configuro o HttpClient com a URL base da API (via appsettings.json), registro os services HTTP tipados e adiciono os componentes Radzen:
| |
A chamada AddRadzenComponents() registra automaticamente DialogService, NotificationService, TooltipService e ContextMenuService no container de DI. Sem ela, os dialogs e notificações não funcionam.
Layout com Radzen
O MainLayout.razor define a estrutura visual da aplicação — header com toggle de sidebar, navegação lateral com RadzenPanelMenu, área de conteúdo e footer:
| |
O componente <RadzenComponents /> no final é obrigatório — ele renderiza os containers para dialogs, notificações e tooltips. Sem ele, DialogService.OpenAsync() e NotificationService.Notify() não exibem nada na tela.
⚠️ Atenção: O
<RadzenComponents />deve estar dentro do layout, não noApp.razor. Colocá-lo fora do layout pode causar problemas de renderização com dialogs e notificações.
Para que o tema visual funcione, o App.razor precisa incluir <RadzenTheme Theme="material" />:
| |
O tema material do Radzen inclui toda a estilização necessária — cores, tipografia, espaçamento, ícones Material Design. Não é necessário importar Bootstrap ou qualquer outro framework CSS.
A API REST — Domínio Produtos
Para o Blazor WASM consumir dados, criei um domínio Produtos com Minimal API no projeto principal. São 10 endpoints organizados em dois grupos:
| Verbo | Rota | Descrição |
|---|---|---|
| GET | /api/produtos?pagina=1&tamanhoPagina=20&filtro= | Listar com paginação e filtro |
| GET | /api/produtos/{id} | Obter por ID |
| POST | /api/produtos | Criar produto |
| PUT | /api/produtos/{id} | Atualizar produto |
| DELETE | /api/produtos/{id} | Remover produto |
| GET | /api/categorias | Listar todas |
| GET | /api/categorias/{id} | Obter por ID |
| POST | /api/categorias | Criar categoria |
| PUT | /api/categorias/{id} | Atualizar categoria |
| DELETE | /api/categorias/{id} | Remover categoria |
O ProdutoEndpoints.cs usa MapGroup para organizar as rotas e ProducesResponseType para documentar no Swagger:
| |
Os DTOs de request usam DataAnnotations para validação server-side, garantindo que a API valide os dados mesmo que o client-side seja bypassed:
| |
A implementação do service usa ConcurrentDictionary como storage in-memory (decisão de design para manter o tutorial focado no Blazor WASM, sem dependência de banco de dados). O seed inicial inclui 8 categorias e mais de 50 produtos distribuídos entre elas.
Configuração CORS
Como o Blazor WASM Standalone roda em uma porta diferente da API (5200 vs 5101), é obrigatório configurar CORS no Program.cs da API:
| |
💡 Dica: Configurar CORS é obrigatório para Blazor WASM Standalone. Sem configuração explícita, o browser bloqueará as requisições cross-origin. Em produção, substitua os origins por domínios reais.
Para testar a API isoladamente, rode dotnet run --project src/BlogSamples e acesse http://localhost:5101/docs — o Swagger mostra todos os endpoints de Produtos e Categorias.
O diagrama abaixo mostra a arquitetura completa — o Blazor WASM no browser se comunica com a API REST via HttpClient (JSON) atravessando a barreira de CORS:

Services HTTP — Consumindo a API
O pattern que uso para consumir a API é service tipado com HttpClient injetado via primary constructor. Cada service encapsula as chamadas HTTP para um domínio específico, usando os métodos de extensão do System.Net.Http.Json — GetFromJsonAsync, PostAsJsonAsync e PutAsJsonAsync:
| |
Alguns pontos importantes sobre este pattern:
Uri.EscapeDataStringno filtro previne injeção de parâmetros na query string. Nunca concatene strings diretamente em URLs sem encoding.EnsureSuccessStatusCode()lançaHttpRequestExceptionse a API retornar erro (4xx, 5xx). No componente Blazor, capturo essa exceção para exibir notificação de erro ao usuário.- Primary constructor (
HttpClient http) evita o boilerplate de campo + construtor. OHttpClienté resolvido pelo container de DI com aBaseAddressconfigurada noProgram.cs.
O CategoriaApiService segue exatamente o mesmo pattern, com métodos ListarAsync, CriarAsync, AtualizarAsync e RemoverAsync.
No Angular, HttpClient com interceptors e operadores RxJS oferece ergonomia similar. Em Blazor, a experiência é equivalente — DelegatingHandler serve como interceptor para autenticação, logging ou retry. A diferença principal é que Blazor usa async/await nativo do C# em vez de Observable do RxJS.
ℹ️ Informação: Os models do Blazor WASM são classes (não records). Radzen Blazor usa two-way binding (
@bind-Value) que requer setters mutáveis. Records cominitnão funcionam para edição em formulários Radzen.
DataGrid de Produtos com Radzen
O RadzenDataGrid é o componente central deste tutorial. Ele suporta paginação server-side, sorting, filtering, templates customizados por coluna e integração direta com o pattern de LoadData — um callback que o grid chama toda vez que precisa de dados novos (ao mudar de página, ordenar ou filtrar).
Aqui está o Produtos.razor completo — vou explicar cada parte:
| |
Vou detalhar os pontos-chave:
LoadData="@CarregarDados"— o grid chama este callback ao inicializar, ao mudar de página e ao ordenar. RecebeLoadDataArgscomSkip,Tope informações de ordering. Essa é a chave para paginação server-side.Data+Count—Datarecebe a página atual de itens;Countinforma o total de registros. O grid calcula o número de páginas automaticamente.FormatString="{0:C2}"— formata o preço como moeda. O Blazor WASM usa a cultura configurada no browser, então em pt-BR exibe “R$ 1.299,00”.Template— colunas customizadas. UsoRadzenBadgepara exibir o status como badge verde/cinza e botões de ação (Editar, Excluir) com ícones Material Design.IsLoading— exibe um spinner enquanto a API está sendo chamada. Melhora significativamente a UX em conexões lentas.
O @code block contém a lógica:
| |
O método CarregarDados converte Skip/Top (pattern do Radzen) para pagina/tamanhoPagina (pattern da minha API). Quando o usuário digita no campo de busca, OnFiltroChanged volta para a primeira página com grid.FirstPage(true) — o true força o reload que chama CarregarDados novamente com o filtro atualizado.
⚠️ Atenção: O evento
LoadDatadoRadzenDataGridé chamado toda vez que o grid precisa de dados — paging, sorting, filtering. Não confunda comDatabinding direto, que é para dados client-side. Se você usarDatacom uma lista completa eAllowPaging, a paginação será client-side (todos os dados carregados de uma vez). ComLoadData+Count, a paginação é server-side (apenas a página atual é carregada).
Formulário de Criação e Edição
O ProdutoForm.razor é um componente reutilizável que serve tanto para criar quanto para editar produtos. A distinção é feita pelo Parameter ProdutoId: se for null, é criação; se tiver valor, é edição. O formulário é aberto em um dialog modal via DialogService.OpenAsync<ProdutoForm>().
| |
Vou destacar os pontos mais importantes do formulário:
RadzenTemplateForm<CriarProdutoRequest>— encapsula todo o formulário com validação. OSubmitevent só é disparado se todos os validators passarem.RadzenRequiredValidatoreRadzenNumericRangeValidator— validação client-side com feedback visual automático (bordas vermelhas, mensagem de erro abaixo do campo). A validação server-side via DataAnnotations na API é a segunda camada de proteção.RadzenDropDown<int>para categorias —Datarecebe a lista,TextPropertyeValuePropertymapeiam as propriedades do DTO.AllowFiltering="true"habilita busca inline no dropdown (útil quando há muitas categorias).RadzenSwitchcom label dinâmico — exibe “Ativo” ou “Inativo” conforme o estado do toggle.- Botão Cancelar com
ButtonType="ButtonType.Button"— sem isso, o click do Cancelar dispara o submit do formulário.
O @code block contém a lógica de inicialização e submit:
| |
O fluxo completo é: usuário clica “Novo Produto” → DialogService.OpenAsync<ProdutoForm>() abre o dialog → o form carrega categorias e, se for edição, carrega o produto → usuário preenche/edita → validators verificam → OnSubmit chama a API → DialogService.Close(true) fecha o dialog → o grid detecta resultado is true e chama grid.Reload() → dados atualizados.
Para abrir o dialog a partir de Produtos.razor:
| |
O dicionário { "ProdutoId", produtoId } passa o parâmetro para o componente ProdutoForm. O DialogOptions controla a largura do dialog e se ele fecha ao clicar fora ou pressionar Escape.
📝 Exemplo: Fluxo de edição — clique no botão “Editar” de um produto → dialog abre com título “Editar Produto” → campos preenchidos com dados atuais → altere o preço → clique Salvar → notificação verde “Produto atualizado” → grid recarrega com preço novo.
Exclusão com Confirmação e Notificações
Toda operação destrutiva deve ter uma etapa de confirmação. O DialogService.Confirm() do Radzen renderiza um dialog nativo com botões customizáveis:
| |
As notificações do Radzen (NotificationService.Notify) aparecem como toasts no canto da tela. Uso NotificationSeverity.Success com duração de 4 segundos para operações bem-sucedidas e NotificationSeverity.Error com 6 segundos para erros — mais tempo para o usuário ler a mensagem. O pattern de try/catch com HttpRequestException é simples mas eficaz: se a API retornar erro (ex: produto não encontrado), o catch exibe feedback imediato ao usuário.
Gerenciamento de Categorias — Inline Editing
Para demonstrar um pattern alternativo ao dialog, a tela de Categorias usa inline editing — o usuário edita diretamente na grid, sem abrir modal. Esse approach funciona bem para entidades simples com poucos campos.
| |
A diferença principal está no EditMode="DataGridEditMode.Single": quando o usuário clica em “Editar”, a linha entra em modo de edição — os campos de texto e o switch aparecem no lugar dos valores estáticos. Os botões mudam de “Editar/Excluir” para “Salvar/Cancelar”.
A lógica de edição inline:
| |
O fluxo para “Nova Categoria” é: inserir um objeto vazio na posição 0 da lista → grid.EditRow() coloca essa linha em modo de edição → usuário preenche → SalvarLinha chama grid.UpdateRow() → OnRowCreate é disparado (porque Id == 0) → API chamada → dados recarregados.
Quando usar inline editing vs dialog:
| Cenário | Inline Editing | Dialog |
|---|---|---|
| Entidades simples (2-4 campos) | ✅ Ideal | Overkill |
| Entidades complexas (5+ campos, dropdowns) | Confuso | ✅ Ideal |
| Campos com validação elaborada | Limitado | ✅ Mais espaço visual |
| Edição rápida e frequente | ✅ Menos cliques | Mais cliques |
| UX mobile | ⚠️ Pode ficar apertado | ✅ Melhor em telas pequenas |
Dicas e Boas Práticas
Centralize chamadas HTTP em services tipados — nunca injete
HttpClientdiretamente no componente.razor. Isso viola separação de responsabilidades e dificulta testes. Services comoProdutoApiServiceencapsulam URLs, serialização e tratamento de erros em um único lugar reutilizável.Use DataAnnotations + validators Radzen para validação dupla —
RadzenRequiredValidatoreRadzenNumericRangeValidatorvalidam no client; DataAnnotations no DTO de request validam no server. Se alguém bypassar o UI e chamar a API diretamente, a validação server-side ainda protege os dados.RadzenNotification para TODA ação — feedback visual consistente em sucesso (“Produto criado”) e erro (“Não foi possível salvar”). Defina duração diferente: 4 segundos para sucesso, 6+ para erros (o usuário precisa de mais tempo para ler a mensagem de erro).
LoadData event para paginação server-side — nunca carregue todos os dados no client com uma lista completa. Com
LoadData, apenas a página atual é transferida pela rede. Para um catálogo com 10.000 produtos, carregar tudo na memória do browser é inviável; com paginação server-side, cada request traz apenas 20 registros.Componentize forms reutilizáveis —
ProdutoFormserve para criação E edição. A distinção é umParameternullable (int? ProdutoId). Esse pattern elimina duplicação de código e garante consistência entre os fluxos de criação e edição.Configure
appsettings.jsonpara URL da API — nunca faça hardcode de URLs. Owwwroot/appsettings.jsonno Blazor WASM funciona como arquivo de configuração por ambiente. Em produção, substitua porappsettings.Production.jsoncom a URL real da API.Implemente loading state no DataGrid — a propriedade
IsLoadingdoRadzenDataGridexibe um spinner enquanto a API é chamada. Sem feedback visual, o usuário não sabe se a ação foi disparada ou se a aplicação travou. DefinaisLoading = trueantes da chamada efalsedepois.Configure AOT + Trimming para produção — o bundle size do Blazor WASM é uma preocupação real. Para produção, habilite AOT compilation e trimming no
.csproj:1 2 3 4<PropertyGroup> <RunAOTCompilation>true</RunAOTCompilation> <PublishTrimmed>true</PublishTrimmed> </PropertyGroup>AOT compila o IL para WebAssembly nativo (execução mais rápida), e trimming remove código não utilizado (bundle menor). O trade-off é tempo de build significativamente maior.
Conclusão
Neste tutorial, construí um CRUD completo com Blazor WebAssembly .NET 10 e Radzen Blazor: DataGrid com paginação server-side e busca, formulários com validação client-side e server-side, dialogs modais para criação e edição, inline editing para entidades simples, exclusão com confirmação e notificações visuais para feedback — tudo em C#, sem escrever uma linha de JavaScript pelo desenvolvedor.
Radzen Blazor entrega componentes visuais maduros para o cenário CRUD corporativo. O RadzenDataGrid com LoadData resolve paginação server-side de forma elegante. Os validators (RadzenRequiredValidator, RadzenNumericRangeValidator) funcionam bem para validação básica. O DialogService e NotificationService cobrem o fluxo completo de interação com o usuário.
Mas é importante ser transparente sobre as limitações que encontrei durante o desenvolvimento:
Radzen.Blazor.jsé necessário — apesar do slogan “zero JavaScript”, a biblioteca depende de JS interno para renderizar componentes complexos. Não é JavaScript escrito pelo desenvolvedor, mas é JS rodando no browser.- Two-way binding requer classes, não records —
@bind-Valuedo Radzen precisa de setters mutáveis. Records cominitnão funcionam para formulários de edição. Isso força o uso de classes para DTOs no Blazor WASM. - O bundle size continua significativo — o runtime do .NET + Radzen + a aplicação resultam em um download inicial considerável. AOT e trimming ajudam, mas não resolvem completamente.
Como analisei no artigo anterior, Blazor WASM é viável para contextos corporativos — e este tutorial demonstra que o ecossistema de componentes suporta cenários reais. A decisão entre Blazor WASM e frameworks JavaScript como Angular ou React depende do perfil da equipe, requisitos de SEO/SSR e tolerância ao bundle size, conforme discuti no comparativo.
Clone o repositório, rode a API e o Blazor WASM, e forme sua própria opinião. O código completo está em frontend/blazor-wasm/ e src/BlogSamples/Produtos/.
Leia Também
- Seu Próximo Frontend Será C#? A Verdade Sobre Blazor WASM — comparativo teórico Blazor vs Angular (1º artigo da série “Frontend Moderno”)
- Design de APIs REST: Verbos HTTP e Parameter Binding — a estrutura de Minimal API que o Blazor WASM consome neste tutorial
- Log Sem Contexto é Ruído: Logging Estruturado no .NET 8 — ecossistema .NET maduro com logging, métricas e tracing
- Autenticação e Autorização: JWT, OAuth2 e OpenID Connect — próximo passo: proteger a SPA Blazor com Entra ID
Referências
- Blazor WebAssembly — Documentação oficial — Microsoft Learn, referência completa sobre Blazor
- ASP.NET Core 10.0 Release Notes — novidades do .NET 10 para web
- Radzen Blazor Components — Get Started — setup e guia inicial da biblioteca
- Radzen Blazor — GitHub — código fonte (MIT) com exemplos e issues
- Radzen DataGrid — documentação e demos interativos do componente central
- blog-zocateli-sample — GitHub — repositório com todo o código deste artigo
- WebAssembly — especificação oficial do padrão W3C
- Blazor WASM Standalone Deployment — guia de deploy para produção

Ao comentar, você concorda com nossa Política de Privacidade, Termos de Uso e Política de Exclusão de Dados.