Minimal APIs + SSE: enviar progresso de jobs para clientes Blazor passo a passo
Por John Pires · 23 de março de 2026

Minimal APIs + SSE: enviar progresso de jobs para clientes Blazor passo a passo
Introducao ao problema
O que queremos resolver
Você tem um servidor ASP.NET Core que executa jobs demorados (importações, processamento de imagens, geração de relatórios). Precisamos informar o cliente em tempo real sobre o progresso dessas tarefas, sem forçar refreshs ou polling agressivo. Blazor (Server ou WebAssembly) deve receber updates do servidor assim que eles acontecem.
Porque usar Minimal APIs e SSE
Minimal APIs simplificam a criação de endpoints em ASP.NET Core com pouco boilerplate, ideal para endpoints pequenos como um stream de eventos. Server-Sent Events (SSE) é um protocolo leve baseado em HTTP para enviar mensagens unidirecionais do servidor para o cliente. É mais simples que WebSockets para cenários de push simples (progresso, logs, notificações).
Esta combinação permite criar um endpoint /jobs/{id}/events que mantém uma conexão aberta e envia linhas no formato SSE: cada evento é prefixado por data: e termina em dupla quebra de linha. No cliente Blazor, podemos usar EventSource (via JS) para consumir esses eventos com muito pouco código.
Tip: SSE funciona bem em HTTP/1.1 e é fácil de depurar (você vê o fluxo raw no DevTools). Para comunicação bidirecional ou alta taxa, considere WebSockets.
Pre-requisitos e setup
Ferramentas e versões
Para seguir este guia você precisa de:
SDK .NET 7 ou superior
Visual Studio 2022 / VS Code
Conhecimentos básicos de C#, HTTP e Blazor
Navegador moderno com suporte a EventSource (Chrome, Edge, Firefox)
Projeto inicial
Crie uma solução com dois projetos: um ASP.NET Core Minimal API (server) e um Blazor WebAssembly (client). No servidor você vai expor endpoints para disparar jobs e para consumir o SSE. No cliente Blazor, usando EventSource via JS ou JS interop, vamos receber os eventos.
Comandos para criar a estrutura (terminal):
dotnet new web -o JobServer
cd JobServer
dotnet new sln -n JobSolution
dotnet sln add JobServer/JobServer.csproj
dotnet new blazorwasm -o JobClient --hosted false
dotnet sln add JobClient/JobClient.csprojEsses comandos iniciam o servidor e o cliente. Vamos focar no servidor e no componente Blazor que consome o SSE. O cliente pode ser servido por um servidor estático ou pelo próprio SDK para desenvolvimento.
Tutorial guiado passo a passo
1) Implementando um job runner simples
Vamos criar um serviço que simula jobs longos e que reporta progresso via um mecanismo de publicação/assinatura. Usaremos channels ou IObservable, mas aqui mostrarei uma implementação com um registry simples e callbacks.
O código abaixo implementa um JobService com registro de listeners e execução simulada de job. Cada passo notifica o progresso.
using System.Collections.Concurrent;
using System.Text.Json;
public class JobProgress
{
public string JobId { get; set; }
public int Percent { get; set; }
public string Message { get; set; }
}
public class JobService
{
private readonly ConcurrentDictionary>> _listeners = new();
public string StartJob()
{
var id = Guid.NewGuid().ToString();
_ = RunJobAsync(id); // fire and forget (para demo)
return id;
}
public void Subscribe(string jobId, Func handler)
{
var list = _listeners.GetOrAdd(jobId, _ => new List>());
list.Add(handler);
}
public void Unsubscribe(string jobId, Func handler)
{
if (_listeners.TryGetValue(jobId, out var list))
list.Remove(handler);
}
private async Task RunJobAsync(string jobId)
{
for (int i = 0; i <= 100; i += 10)
{
var progress = new JobProgress { JobId = jobId, Percent = i, Message = $"Step {i / 10}" };
if (_listeners.TryGetValue(jobId, out var list))
{
foreach (var handler in list.ToArray())
{
try { await handler(progress); } catch { /* ignore listener errors */ }
}
}
await Task.Delay(500); // simula work
}
}
}
Contexto: esse serviço simula um processo que envia atualizações a cada 500ms. O registro de Func<JobProgress, Task> permite que múltiplos clientes ou endpoints escutem o progresso.
O que acontece depois: qualquer listener registrado para o jobId receberá callbacks com objetos JobProgress. Em produção, substitua a simulação por execução real.
2) Endpoint SSE em Minimal API
Agora criamos o endpoint SSE que mantém a conexão e envia eventos no formato correto. O endpoint vai subscrever o JobService e escrever mensagens no Response Body.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Text;
var builder = WebApplication.CreateBuilder();
builder.Services.AddSingleton();
var app = builder.Build();
app.MapPost("/jobs/start", (JobService svc) => Results.Ok(new { id = svc.StartJob() }));
app.MapGet("/jobs/{id}/events", async (string id, HttpResponse response, JobService svc, CancellationToken ct) =>
{
response.Headers.Add("Cache-Control", "no-cache");
response.Headers.Add("Content-Type", "text/event-stream");
// Listener que escreve no response
async Task Handler(JobProgress p)
{
if (ct.IsCancellationRequested) return;
var json = System.Text.Json.JsonSerializer.Serialize(p);
var payload = $"data: {json}\n\n"; // formato SSE
var bytes = Encoding.UTF8.GetBytes(payload);
await response.Body.WriteAsync(bytes.AsMemory(0, bytes.Length), ct);
await response.Body.FlushAsync(ct);
}
// Registra e aguarda cancelamento
svc.Subscribe(id, Handler);
try
{
// Mantém a conexão viva até o cliente desconectar
await Task.Delay(Timeout.Infinite, ct);
}
catch (OperationCanceledException) { }
finally
{
svc.Unsubscribe(id, Handler);
}
});
app.Run();
Contexto: esse endpoint escreve JSON serializado prefixado por data: e termina com uma linha em branco — requisito do SSE. Usamos cancellation token para detectar quando o cliente fecha a conexão.
O que acontece depois: quando o JobService dispara um progresso, o Handler serializa e escreve no corpo da resposta; o cliente receberá em tempo real cada evento.
3) Cliente Blazor consumindo SSE
No Blazor WebAssembly, a forma simples é usar JavaScript EventSource e chamar funções JS via JS interop. Você também pode usar interop direto dentro do .razor com código JS embutido. Abaixo há um exemplo de componente Blazor que conecta ao SSE e exibe o progresso.
@page "/job-progress"
@inject IJSRuntime JS
Job Progress Start Job
@foreach(var p in events)
{
@(p.Percent)% - @p.Message
}
@code {
private List events = new();
private DotNetObjectReference<JobProgressHandler> objRef;
private async Task StartJob()
{
var client = new HttpClient { BaseAddress = new Uri(NavigationManager.BaseUri) };
var resp = await client.PostAsync("/jobs/start", null);
var json = await resp.Content.ReadFromJsonAsync<Dictionary<string,string>>();
var id = json!['id'];
objRef = DotNetObjectReference.Create(new JobProgressHandler(UpdateFromJs));
await JS.InvokeVoidAsync("startSse", $"/jobs/{id}/events", objRef);
}
private void UpdateFromJs(JobProgress p)
{
events.Add(p);
StateHasChanged();
}
public class JobProgressHandler
{
private readonly Action<JobProgress> _callback;
public JobProgressHandler(Action<JobProgress> callback) => _callback = callback;
[JSInvokable]
public void Receive(string json)
{
var p = System.Text.Json.JsonSerializer.Deserialize<JobProgress>(json)!;
_callback(p);
}
}
public class JobProgress { public string JobId { get; set; } public int Percent { get; set; } public string Message { get; set; } }
}
Contexto: o componente dispara um POST para /jobs/start, recebe o id e chama uma função JS chamada startSse passando um DotNetObjectReference. Essa função cria um EventSource e, para cada evento recebido, invoca o método .NET via interop.
O que acontece depois: a UI do Blazor é atualizada por cada evento recebido. Essa abordagem mantém a parte de streaming no JS (EventSource) e a lógica de UI no Blazor.
Tip: Em Blazor Server você pode usar
EventSourcetambém no cliente, mas considere as diferenças de lifecycles; prefira JS interop com limpeza adequada.
Resultado final funcionando
Fluxo completo
Fluxo esperado quando tudo está em execução:
Cliente Blazor envia POST /jobs/start
Servidor cria job e retorna um id
Cliente abre /jobs/{id}/events via EventSource
Servidor executa job, publica progresso para listeners
Endpoint SSE escreve cada progresso no formato
data: {...}\n\nEventSource no cliente dispara callback e Blazor atualiza a UI
Verificações práticas
Para garantir que tudo está certo:
Use DevTools -> Network para ver a conexão /jobs/{id}/events aberta e eventos chegando.
Confirme que os headers incluem
Content-Type: text/event-stream.Em caso de proxies (NGINX), habilite suporte a streaming (disable buffering).
Se você rodar localmente, abra duas abas: uma para iniciar o job e outra para inspecionar o stream. Você verá linhas semelhantes a data: {"JobId":"...","Percent":10,"Message":"Step 1"}.
Erros comuns, armadilhas e dicas práticas
1) Headers e buffering
Um erro comum é esquecer de configurar o header Content-Type para text/event-stream ou ter um proxy que faz buffer e só entrega tudo no final. Isso quebra a percepção de tempo real.
Se estiver usando NGINX, habilite proxy_buffering off; para o endpoint SSE. Em servidores PaaS (Azure), verifique configurações de timeout para conexões longas.
2) Formatação SSE incorreta
SSE exige que cada evento termine com dupla quebra de linha. Se você enviar apenas uma, alguns navegadores aguardam por mais dados e não disparam o evento. Sempre envie data: ...\n\n.
3) Cancelamento e limpeza
Importante: remova listeners quando o cliente desconectar. No exemplo usamos CancellationToken para detectar desconexão e chamamos Unsubscribe no finally. Caso contrário, você acumula listeners e vaza memória.
Dica: durante desenvolvimento, logue quando listeners são adicionados/removidos para detectar vazamentos.
Desafio para o leitor praticar
Desafio 1: Persistir status no banco
Adapte o JobService para salvar o progresso no banco usando EF Core. Crie uma entidade JobStatus contendo JobId, Percent, LastUpdate e Message. Salve cada atualização e permita recuperar o estado via GET /jobs/{id}/status.
Desafio 2: Reconnect e recuperação
Implemente reconexão no cliente: quando o EventSource cair, tente reconectar automaticamente e envie o último percentual conhecido para o servidor para recuperar apenas eventos faltantes. Você pode adicionar um parâmetro ?fromPercent=XX ao URL do SSE para pedir updates a partir daquele ponto.
No servidor, ao iniciar a conexão SSE, consulte o último status no banco e reenvie o último estado imediatamente.
No cliente, armazene o último Percent recebido e, ao reconectar, inclua ?fromPercent=.
Esses exercícios consolidam conceitos de persistência e resiliência, frequentemente necessários em produção.
Conclusao pratica
Resumo dos pontos principais
Usar Minimal APIs + SSE é uma solução direta para enviar updates unidirecionais de servidor para cliente. A arquitetura proposta separa responsabilidades: JobService executa e notifica; endpoint SSE escreve os eventos; Blazor consome via EventSource e atualiza a UI. Essa abordagem reduz complexidade versus WebSockets para casos simples.
Próximos passos
Implemente os desafios, adicione testes e trate cenários de produção: buffering de proxies, escalabilidade (como compartilhar eventos entre instâncias usando Redis Pub/Sub), autenticação do stream (tokens) e limites de reconexão. Com esses cuidados, seu sistema ganha comunicação em tempo real com código simples e robusto.
Finalizando: a combinação é prática, de fácil entendimento e ideal para iniciantes entenderem push serverside. Aplique o padrão em tarefas de background e faça pequenas iterações para tornar a solução pronta para produção.
Assuntos: C#, aspnet-core, blazor, sse, minimal-apis
Leia também
Multitenancy em ASP.NET Core com EF Core: discriminator, schema e database
Comparação prática entre discriminator, schema e database para multitenancy em ASP.NET Core com EF Core. Implementação de TenantProvider, DbContext dinâmico…
Implementando paginação cursor-based com EF Core e Minimal APIs (passo a passo)
Guia prático para implementar paginação cursor-based com EF Core e Minimal APIs: modelagem do cursor, consultas keyset em LINQ, geração segura de cursor…
SignalR com Blazor: criar chat em tempo real com reconexão e fallback
Guia prático em C# para criar um chat com SignalR e Blazor WASM: passo a passo, código real, reconexão automática e fallback para LongPolling quando…
JWT em Minimal APIs (ASP.NET Core): access e refresh tokens do zero
Aprenda do zero como implementar access e refresh tokens com JWT em Minimal APIs do ASP.NET Core: código real, armazenamento de refresh token, endpoints…
Upload de arquivos com ASP.NET Core Minimal APIs: streaming e progresso
Guia prático para implementar upload de arquivos com ASP.NET Core Minimal APIs: streaming para evitar OOM, reporting de progresso via polling e dicas para…
Autenticação JWT em Minimal APIs: refresh tokens seguros com HttpOnly
Guia prático para implementar refresh tokens seguros com cookies HttpOnly em Minimal APIs ASP.NET Core: conceitos, passos comentados, exemplos em C# e…