Server-Sent Events no .NET 10
O suporte nativo a Server-Sent Events no .NET 10 facilita muito a comunicação em tempo real entre servidor e cliente no ASP.NET Core. Quando se fala em aplicações web modernas, manter o cliente sincronizado com o servidor se tornou essencial.
Seja para atualizar o status de um pedido, enviar notificações instantâneas ou transmitir dados de sensores, esse sincronismo vira um diferencial competitivo. A boa notícia é que o ASP.NET Core agora oferece suporte nativo a Server-Sent Events (SSE) em Minimal APIs e controllers, sem nenhum pacote adicional.
O que são Server-Sent Events
Server-Sent Events é uma tecnologia baseada em HTTP que permite ao servidor enviar atualizações automáticas para o cliente por uma única conexão. O navegador abre a requisição uma vez, e o servidor continua escrevendo eventos enquanto a conexão existir.
Diferente do WebSocket, que permite comunicação bidirecional, o SSE é unidirecional. Ou seja, os dados seguem apenas do servidor para o cliente.
Essa abordagem é ideal quando o cliente só precisa ouvir o servidor. Por exemplo, dashboards, notificações, acompanhamento de pedidos e streaming de métricas se encaixam bem nesse modelo.
Se você quiser uma visão geral do protocolo e de exemplos fora do .NET, o artigo sobre o que são Server-Sent Events aprofunda a parte conceitual. Aqui, o foco fica na implementação com ASP.NET Core.
Como o protocolo funciona
O servidor responde com o tipo de conteúdo text/event-stream e envia mensagens em texto UTF-8. Cada mensagem termina com uma linha em branco, e os campos seguem o formato nome: valor.
event: heartRate
id: 42
retry: 60000
data: {"heartRate":72}
Segundo a documentação da MDN sobre Server-Sent Events, o campo event define o tipo do evento, enquanto data carrega o conteúdo. Já o campo id registra o último evento recebido, e o retry informa o tempo de reconexão em milissegundos.
Além disso, linhas que começam com dois pontos funcionam como comentários. Por isso, muitos servidores usam essas linhas para manter a conexão ativa.
O que muda com Server-Sent Events no .NET 10
Antes, quem queria usar SSE no ASP.NET Core precisava escrever o cabeçalho, formatar cada linha e controlar o flush manualmente. Com o .NET 10, o framework ganhou o método TypedResults.ServerSentEvents, que cuida desse trabalho para você.
De acordo com a documentação de novidades do ASP.NET Core no .NET 10, o recurso funciona tanto em Minimal APIs quanto em controllers. Portanto, você usa a mesma API nos dois modelos.
O método aceita uma sequência assíncrona de eventos, que o servidor envia ao cliente conforme os itens ficam prontos. Na prática, o uso de Server-Sent Events no .NET 10 se apoia em dois tipos principais:
IAsyncEnumerable<T>, a sequência assíncrona que produz os dados.SseItem<T>, a estrutura do namespaceSystem.Net.ServerSentEventsque representa um evento.
A estrutura SseItem
Cada SseItem<T> pode carregar quatro informações. Primeiro vem o payload de dados do tipo T, exposto na propriedade Data.
Em seguida, temos o tipo do evento, na propriedade EventType. Também existe um ID opcional, em EventId, além do intervalo de reconexão, em ReconnectionInterval.
Essas propriedades correspondem diretamente aos campos data, event, id e retry do protocolo. Assim, você controla todo o formato do evento sem escrever texto manualmente.
As sobrecargas de TypedResults.ServerSentEvents
O método TypedResults.ServerSentEvents tem três sobrecargas. A primeira recebe um IAsyncEnumerable<string> e um tipo de evento opcional.
A segunda recebe um IAsyncEnumerable<T> qualquer, também com um tipo de evento opcional. Por fim, a terceira recebe um IAsyncEnumerable<SseItem<T>>, que dá controle total sobre cada evento.
Um detalhe importante está na serialização. Strings seguem para o cliente como texto puro, enquanto outros tipos passam pelo serializador JSON configurado na aplicação.
Exemplo prático de Server-Sent Events no .NET 10
Imagine que você está construindo uma aplicação de monitoramento de saúde em tempo real. Nesse caso, o servidor precisa enviar batimentos cardíacos simulados continuamente para a tela.
Veja como fica simples retornar essa sequência com Server-Sent Events no .NET 10 usando a sobrecarga com tipo de evento:
using System.Runtime.CompilerServices;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/json-item", (CancellationToken cancellationToken) =>
{
async IAsyncEnumerable<HeartRateRecord> GetHeartRate(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var heartRate = Random.Shared.Next(60, 100);
yield return HeartRateRecord.Create(heartRate);
await Task.Delay(2000, cancellationToken);
}
}
return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken),
eventType: "heartRate");
});
app.Run();
// Registro com o valor do batimento e o horário da leitura
public record HeartRateRecord(int HeartRate, DateTime Timestamp)
{
public static HeartRateRecord Create(int heartRate) =>
new(heartRate, DateTime.UtcNow);
}
Nesse exemplo, criamos o endpoint /json-item, que responde com uma sequência infinita de dados JSON. A cada 2 segundos, um novo valor de batimento surge e segue para o cliente.
Todo evento enviado tem o tipo heartRate. Além disso, o CancellationToken encerra o laço quando o cliente fecha a conexão, o que evita trabalho desnecessário no servidor.
Repare também no atributo [EnumeratorCancellation]. Ele conecta o token recebido pelo endpoint ao iterador assíncrono, então o cancelamento chega até o Task.Delay.
Controlando cada evento com SseItem
Quando você precisa de mais controle, a sequência pode produzir objetos SseItem<T>. A documentação oficial mostra esse formato no mesmo cenário de batimentos cardíacos.
app.MapGet("/sse-item", (CancellationToken cancellationToken) =>
{
async IAsyncEnumerable<SseItem<int>> GetHeartRate(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var eventId = 0;
while (!cancellationToken.IsCancellationRequested)
{
var heartRate = Random.Shared.Next(60, 100);
// Cada evento leva tipo, id e intervalo de reconexão
yield return new SseItem<int>(heartRate, eventType: "heartRate")
{
EventId = (++eventId).ToString(),
ReconnectionInterval = TimeSpan.FromMinutes(1)
};
await Task.Delay(2000, cancellationToken);
}
}
return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken));
});
Nesse caso, cada evento recebe um identificador sequencial. Com isso, os Server-Sent Events no .NET 10 permitem que o navegador informe o último evento recebido quando a conexão volta.
O ReconnectionInterval também orienta o cliente. Afinal, ele define quanto tempo o navegador deve esperar antes de tentar reconectar.
Para usar SseItem<T>, adicione using System.Net.ServerSentEvents; no topo do arquivo. Esse namespace já acompanha o ASP.NET Core, então Server-Sent Events no .NET 10 não exigem pacotes extras.
Server-Sent Events no .NET 10 com controllers
Nem todo projeto usa Minimal APIs, e isso não impede o uso do recurso. Afinal, Server-Sent Events no .NET 10 também funcionam em APIs baseadas em controllers.
Nesse modelo, a action devolve o mesmo resultado criado por TypedResults.ServerSentEvents. Como ele implementa IResult, o MVC executa a resposta da mesma forma que a Minimal API.
using System.Net.ServerSentEvents;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class HeartRateController : ControllerBase
{
[HttpGet]
public IResult Get(CancellationToken cancellationToken)
{
// Reaproveita o mesmo iterador assíncrono da versão Minimal API
return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken));
}
private static async IAsyncEnumerable<SseItem<int>> GetHeartRate(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var heartRate = Random.Shared.Next(60, 100);
yield return new SseItem<int>(heartRate, eventType: "heartRate");
await Task.Delay(2000, cancellationToken);
}
}
}
Repare que a lógica de geração dos eventos fica igual nos dois modelos. Portanto, migrar um endpoint de controller para Minimal API, ou o contrário, exige pouquíssimas mudanças.
A própria documentação oficial mantém exemplos de Server-Sent Events no .NET 10 para os dois estilos. Assim, você escolhe o formato que combina com o restante do projeto.
Testando Server-Sent Events no .NET 10 com curl
Antes de escrever qualquer código no cliente, vale conferir o fluxo pelo terminal. O curl resolve isso com a opção -N, que desativa o buffer de saída.
# Exibe os eventos conforme o servidor envia
curl -N https://localhost:5001/sse-item
Com esse comando, cada evento aparece no terminal assim que o servidor o escreve. Dessa forma, você confere os campos event, id, retry e data exatamente como trafegam na rede.
Esse teste também ajuda a diagnosticar problemas de buffer em proxies. Se os eventos chegam todos de uma vez pelo proxy, mas aparecem um a um direto no servidor, a causa está na infraestrutura.
Consumindo os eventos no cliente
O cliente precisa estar preparado para receber e interpretar os eventos. Em JavaScript, a forma mais comum é usar a API EventSource, disponível em todos os navegadores modernos.
const source = new EventSource("/json-item");
// Eventos com tipo definido exigem addEventListener
source.addEventListener("heartRate", (event) => {
const record = JSON.parse(event.data);
console.log(`Heart rate: ${record.heartRate}`);
});
source.onerror = () => {
console.log("Connection lost, the browser will retry");
};
Um detalhe costuma gerar confusão. O manipulador onmessage recebe apenas eventos sem tipo, então eventos com eventType precisam de addEventListener com o nome correto.
Além disso, o EventSource reconecta automaticamente quando a conexão cai. Para encerrar de vez, basta chamar source.close().
Cliente em C# com SseParser
Nem todo consumidor roda em um navegador. Por isso, o namespace System.Net.ServerSentEvents também oferece o SseParser, que lê eventos a partir de um Stream.
using System.Net.ServerSentEvents;
using var client = new HttpClient();
using var stream = await client.GetStreamAsync("https://localhost:5001/sse-item");
// Lê cada evento conforme ele chega pela conexão
await foreach (SseItem<string> item in SseParser.Create(stream).EnumerateAsync())
{
Console.WriteLine($"{item.EventType}: {item.Data}");
}
Com esse cliente, serviços de backend, ferramentas de linha de comando e testes de integração conseguem consumir Server-Sent Events no .NET 10 sem bibliotecas externas.
Como funciona por trás dos panos
O método TypedResults.ServerSentEvents devolve um ServerSentEventsResult<T>, que implementa IResult. Quando o ASP.NET Core executa esse resultado, ele define o tipo text/event-stream e mantém a resposta aberta.
Em seguida, o resultado percorre o IAsyncEnumerable e escreve cada evento no formato do protocolo assim que o item fica pronto. Dessa forma, o cliente recebe os dados sem esperar o fim da sequência.
Esse modelo de Server-Sent Events no .NET 10 é ideal para eventos contínuos, notificações push e streams de dados longos. Inclusive, ele aproveita o mesmo conceito de streaming assíncrono que o artigo sobre async streaming no ASP.NET explora.
Para habilitar o recurso, basta usar o .NET 10 e manter as dependências atualizadas. Não é necessário nenhum pacote adicional para usar TypedResults.ServerSentEvents.
Cuidados ao usar Server-Sent Events no .NET 10
Embora a implementação de Server-Sent Events no .NET 10 seja simples, alguns cuidados evitam surpresas em produção. O primeiro envolve o limite de conexões do navegador.
Segundo a MDN, sem HTTP/2 os navegadores permitem apenas seis conexões SSE por domínio, somando todas as abas abertas. Portanto, servir a aplicação com HTTP/2 faz bastante diferença, já que o limite passa a ser negociado entre cliente e servidor.
No Kestrel, os endpoints aceitam HTTP/1.1 e HTTP/2 por padrão, mas o HTTP/2 depende de TLS. Por isso, publique a API com HTTPS para que o navegador consiga negociar o protocolo mais eficiente.
Outro ponto importa bastante: proxies e balanceadores podem armazenar a resposta em buffer. Nesse caso, os eventos chegam atrasados ou todos de uma vez, e por isso vale revisar a configuração da infraestrutura.
Também convém usar o EventId sempre que a perda de eventos for um problema. Ao reconectar, o navegador envia o cabeçalho Last-Event-ID, e o servidor pode retomar a partir dali.
Por fim, conexões abertas por muito tempo consomem recursos. Assim, respeitar o CancellationToken e limitar a duração dos fluxos ajuda a manter o servidor saudável.
O artigo sobre os desafios dos Server-Sent Events em produção aprofunda esses pontos com mais detalhes.
Comparação com outras tecnologias
Server-Sent Events no .NET 10 não substituem todas as outras opções de tempo real. Na verdade, cada tecnologia atende a um tipo de cenário.
| Tecnologia | Direção | Protocolo | Ideal para |
|---|---|---|---|
| SSE | Unidirecional | HTTP | Notificações, dados contínuos |
| WebSockets | Bidirecional | WebSocket sobre TCP, iniciado por HTTP | Chats, jogos em tempo real |
| Long Polling | Unidirecional | HTTP | Compatibilidade com navegadores antigos |
| SignalR (.NET) | Bidirecional | Abstração sobre os transportes acima | Comunicação rica com fallback |
Quando o cliente também precisa enviar mensagens com frequência, o WebSocket costuma fazer mais sentido. Para conhecer essa alternativa, veja o artigo sobre WebSockets no ASP.NET.
Já o SignalR adiciona grupos, reconexão e escolha automática de transporte. Inclusive, ele pode usar Server-Sent Events como um dos transportes, e o artigo sobre SignalR com Minimal APIs mostra como começar.
Quando escolher Server-Sent Events no .NET 10
Na prática, vale escolher SSE quando o fluxo é do servidor para o cliente e você quer manter tudo em HTTP comum. Isso inclui painéis de monitoramento, barras de progresso de processos longos e feeds de notificações.
Além disso, o SSE funciona bem com a infraestrutura existente, porque usa requisições HTTP normais. Por outro lado, se o cliente precisa enviar dados com frequência, outra tecnologia tende a encaixar melhor.
Pense, por exemplo, em um processo de importação que leva alguns minutos. O cliente dispara a importação por um endpoint comum e, em seguida, abre uma conexão SSE para acompanhar o percentual concluído.
Outro cenário comum envolve respostas de modelos de IA que chegam em partes. Nesse caso, o servidor repassa cada trecho gerado ao navegador, e o usuário acompanha o texto sendo escrito.
Também vale considerar a simplicidade de autenticação. Como o SSE usa requisições HTTP normais, cookies e políticas de autorização do ASP.NET Core continuam funcionando no endpoint.
Em resumo, Server-Sent Events no .NET 10 oferecem o caminho mais curto para tempo real unidirecional. Você escreve um IAsyncEnumerable, devolve o resultado e o framework cuida do restante.
Conclusão
Com a chegada do TypedResults.ServerSentEvents, o ASP.NET Core dá mais um passo em direção a aplicações reativas e responsivas. O framework agora entrega o protocolo pronto, tanto em Minimal APIs quanto em controllers.
A simplicidade do IAsyncEnumerable e a integração com Minimal APIs tornam os Server-Sent Events no .NET 10 uma excelente opção. Assim, você ganha comunicação em tempo real do servidor para o cliente sem a complexidade do WebSocket.
Se você está começando com o modelo de endpoints usado nos exemplos, o artigo sobre Minimal APIs no ASP.NET é um bom ponto de partida.
FAQ
Preciso instalar algum pacote para usar SSE no .NET 10?
Não, o TypedResults.ServerSentEvents já faz parte do ASP.NET Core no .NET 10. Basta usar o SDK do .NET 10 no projeto.
SSE funciona com controllers?
Sim, o recurso funciona tanto em Minimal APIs quanto em controllers. Em ambos os casos, você retorna o resultado de TypedResults.ServerSentEvents.
Por que meu onmessage não recebe os eventos?
Provavelmente os eventos têm um eventType definido. Nesse caso, use addEventListener com o mesmo nome do tipo do evento.
Qual a diferença entre SSE e WebSocket?
O SSE envia dados apenas do servidor para o cliente usando HTTP comum. Já o WebSocket permite comunicação nos dois sentidos com um protocolo próprio.
O navegador reconecta sozinho quando a conexão cai?
Sim, o EventSource reconecta automaticamente e envia o cabeçalho Last-Event-ID quando os eventos têm identificador. O campo retry ajusta o tempo de espera.