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 namespace System.Net.ServerSentEvents que 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.