DEV Community

Cover image for SignalR no .NET: do primeiro Hub à produção
Lincoln Zocateli
Lincoln Zocateli

Posted on Originally published at zocate.li

SignalR no .NET: do primeiro Hub à produção

Introdução

SignalR no .NET é uma biblioteca do ASP.NET Core para comunicação em tempo real entre servidor e clientes. Ela negocia um transporte compatível e oferece uma API de alto nível para chamadas entre browser e servidor, envio de eventos, grupos e streaming. Eu a escolheria quando a interface precisa receber atualizações com baixa latência ou enviar comandos interativos sem transformar cada mudança em uma nova consulta periódica. SignalR simplifica o canal; não fornece armazenamento durável, autorização de domínio pronta nem garantia de entrega exatamente uma vez.

Essa diferença muda o desenho: conexões caem, eventos podem se perder e um usuário autenticado ainda pode não ter acesso ao ambiente solicitado. No Dokpod, o browser recebe uma invalidação pequena, consulta o estado pela API REST e só entra no grupo depois que o Hub verifica a autorização. O BFF mantém os tokens fora do navegador; no artigo sobre BFF e segurança em SPAs, explico esse padrão em mais detalhes.

Este é o terceiro artigo da série APIs e Comunicação em .NET. A parte sobre gRPC no .NET 10 explica o canal bidirecional protegido por mTLS entre API e agentes. Já o comparativo Tempo real no Angular e .NET discute em mais detalhe WebSockets, SSE, SignalR e Long Polling. Aqui o foco é outro: ciclo de vida do Hub, chamadas, autorização, grupos, recuperação de assinaturas e decisões operacionais.

ℹ️ Informação: O repositório público do Dokpod não possui uma release ou tag estável no momento da consulta. Os links de código deste artigo apontam para main, que é mutável; os arquivos podem mudar depois desta revisão.

Pré-requisitos

Os exemplos usam ASP.NET Core/.NET 10 e Angular; conhecimento básico de C#, HTTP e autenticação web basta. Os trechos do Dokpod são ilustrativos, não um sample independente.

É útil distinguir quatro termos. Hub é a abstração que expõe métodos do servidor e envia eventos aos clientes. Conexão lógica é a sessão que o SignalR administra, mesmo quando o transporte subjacente muda. Grupo é uma associação efêmera de conexões para envio direcionado. Invalidação é um aviso para o cliente consultar novamente uma fonte de estado, não uma cópia do próprio estado.

O que SignalR abstrai, e o que continua sendo protocolo

SignalR organiza mensagens sobre uma conexão administrada pela biblioteca. O cliente chama métodos do Hub com invoke (e pode aguardar resultado) ou send (sem aguardar o resultado da aplicação). O Hub envia eventos ao chamador, a grupos ou a todos os clientes. Argumentos são serializados pelo protocolo de Hub: JSON é o padrão e MessagePack é uma alternativa binária.

Essa API não é o próprio WebSocket. SignalR negocia um transporte e usa o protocolo de Hub sobre ele: WebSockets é preferido quando disponível; Server-Sent Events (SSE) e Long Polling são alternativas. O fallback depende do suporte e da configuração do cliente, servidor e intermediários. Ele não torna todos os transportes disponíveis em qualquer ambiente nem converte um transporte unidirecional em duplex.

Transporte e formato são camadas distintas: JSON ou MessagePack podem usar um transporte SignalR compatível. WebSockets puros são uma opção quando a aplicação precisa controlar o protocolo, mas então assume detalhes de framing, reconexão e roteamento. O Hub também não é uma sessão de domínio duradoura: instâncias atendem invocações, e estado persistente deve ficar em serviços ou armazenamento. Para enviar mensagens fora de uma invocação, use IHubContext<T>.

Negociação automática é conveniência de compatibilidade, não uma medição de desempenho; valide o transporte real por cliente e ambiente.

Do registro do Hub à autorização por ambiente

No ASP.NET Core, AddSignalR registra a infraestrutura e MapHub expõe o endpoint. O Hub do Dokpod em /hubs/control-plane tem [Authorize], assim como sua rota. Isso impede conexões anônimas, mas autenticação não autoriza o acesso a todo ambiente.

JoinEnvironmentAsync valida o identificador, obtém do contexto autenticado o sub e o access token recebido do BFF e pede ao Keycloak uma decisão para urn:dokpod:environment:{id} com o scope environment:read. Só a decisão permitida leva a Groups.AddToGroupAsync; negação, indisponibilidade ou resultado indeterminado não concedem acesso.

O grupo tem nome determinístico, como environment-{guid}, mas esse nome não é uma credencial. A autorização ocorre na API antes da associação; o identificador recebido não permite ao cliente escolher qualquer grupo. Operações assíncronas recebem ConnectionAborted para cancelar trabalho quando a conexão some.

public async Task JoinEnvironmentAsync(string environmentId)
{
    if (string.IsNullOrWhiteSpace(environmentId) || environmentId.Length > 64 ||
        environmentId.Any(character => !char.IsLetterOrDigit(character) && character != '-'))
        throw new HubException("Identificador de ambiente inválido.");

    var httpContext = Context.GetHttpContext();
    var accessToken = ExtractAccessToken(httpContext);
    var subject = Context.User?.FindFirstValue("sub");
    if (string.IsNullOrWhiteSpace(accessToken) || string.IsNullOrWhiteSpace(subject) ||
        !Guid.TryParse(environmentId, out var environmentGuid))
        throw new HubException("Acesso negado.");

    var decision = await authorizationDecider.DecideAsync(
        $"urn:dokpod:environment:{environmentGuid:D}", EnvironmentResourceScopes.Read,
        AuthenticatedActor.FromSubject(subject), accessToken, GetCorrelationId(httpContext),
        Context.ConnectionAborted);
    if (!decision.Allowed) throw new HubException("Acesso negado.");

    await Groups.AddToGroupAsync(Context.ConnectionId, GroupFor(environmentId),
        Context.ConnectionAborted);
}
Enter fullscreen mode Exit fullscreen mode

📂 Código-fonte: ControlPlaneHub.cs e ApiHost.cs.

O Hub é apropriado para validações ligadas ao chamador e para coordenar esse fluxo curto. Regras maiores ficam em serviços de aplicação: assim, autorização, auditoria, limites e efeitos de domínio não dependem de uma conexão específica. Se várias invocações concorrerem, a implementação não deve assumir que chamadas da mesma conexão serão serializadas em todos os cenários; estado compartilhado precisa de sincronização e de um dono explícito.

Cliente Angular pelo BFF, sem token no browser

O browser usa uma URL same-origin. O HubConnectionBuilder registra handlers, habilita reconexão e envia o cookie de sessão; antes, o serviço obtém do BFF um token antiforgery para X-Dokpod-Antiforgery. O cookie é HttpOnly: o Angular não lê nem encaminha o Bearer da API.

O BFF autentica a sessão e acrescenta o token obtido do ticket server-side apenas no salto BFF → API. No WebSocket downstream, o Bearer vai em Authorization, nunca de volta ao browser. O BFF protege o POST de negociação contra CSRF e valida Origin; a API ainda autoriza cada ambiente.

invoke aguarda a conclusão da invocação e rejeita a promise em caso de falha. send não confirma a conclusão da regra de negócio. Registre handlers antes de start para não perder eventos recebidos logo após a conexão.

const { requestToken } = await firstValueFrom(
  this.http.get<AntiforgeryResponse>('bff/antiforgery'),
);
const connection = new HubConnectionBuilder()
  .withUrl(controlPlaneHubUrl(document.baseURI), {
    headers: { 'X-Dokpod-Antiforgery': requestToken },
    withCredentials: true,
  })
  .withAutomaticReconnect()
  .build();

connection.on('invalidated', (notification: { environmentId: string; kind: InvalidationKind }) =>
  this.publish(notification.environmentId, notification.kind));
connection.onreconnected(() => void this.rejoinEnvironment(connection, environmentId));
await connection.start();
await connection.invoke('JoinEnvironmentAsync', environmentId);
Enter fullscreen mode Exit fullscreen mode

📂 Código-fonte: console-realtime.service.ts e BffHost.cs.

O snippet omite limpeza e tratamento de estado. No serviço real, uma falha de start() é tratada como erro inicial; withAutomaticReconnect() cobre uma conexão já estabelecida que caiu. A interface deve distinguir esses casos, sem inferir conectividade pela existência do serviço.

Grupos e invalidações: aviso rápido, leitura durável

Depois da autorização, o cliente entra no grupo do ambiente. Quando uma projeção muda, SignalRControlPlaneNotifier usa IHubContext<ControlPlaneHub> para enviar invalidated; o publicador não instancia um Hub nem depende de uma chamada do browser.

O payload contém somente identificador do ambiente e tipo de recurso, como inventory, events ou alerts. O notifier agrega emissões por ambiente e tipo, inicialmente a uma por segundo. A tela consulta então o endpoint REST, que autoriza novamente e devolve a representação atual. SignalR reduz latência sem duplicar a fonte de verdade.

await hubContext.Clients
    .Group(ControlPlaneHub.GroupFor(environmentId.ToString("D")))
    .SendAsync("invalidated",
        new InvalidationNotification(environmentId, kind),
        CancellationToken.None);
Enter fullscreen mode Exit fullscreen mode

📂 Código-fonte: SignalRControlPlaneNotifier.cs e AgentControlService.cs.

Um grupo não é fila, histórico ou assinatura durável. Sua associação pode desaparecer com desconexão ou reinício. Não envie snapshots grandes nem interprete a ausência de aviso como prova de que nada mudou; na volta, busque o estado atual pela API.

O retorno de SendAsync também não confirma que o browser recebeu ou processou a invalidação. Se várias alterações forem agregadas, a leitura REST ainda precisa convergir para a representação mais recente.

Autenticação, autorização e limites de confiança

[Authorize] protege a entrada, mas não a autorização de cada recurso. O principal fica associado à conexão e mudanças de roles ou claims não são automaticamente revalidadas em toda invocação. O Dokpod consulta Keycloak ao entrar no grupo e reavalia permissões em alguns fluxos sensíveis de longa duração.

O browser usa cookie same-origin através do BFF, que acrescenta o Bearer somente server-side. Em uma conexão direta autenticada por bearer, APIs de WebSocket e SSE do navegador não permitem configurar livremente Authorization; o cliente pode colocar access_token na query string. HTTPS protege o tráfego, mas URLs podem aparecer em logs. Se essa opção for inevitável, restrinja-a à rota do Hub e remova a query dos logs. O relay do Dokpod evita expor o token ao browser ou à URL downstream.

Cookies também criam risco de CSRF. O BFF valida Origin, restringe origens e exige antiforgery no POST de negociação. CORS controla chamadas cross-origin do browser, mas não autentica usuários nem autoriza ambientes; same-origin tampouco elimina validação de Origin para uma conexão por cookie.

Grupos não são fronteiras de segurança: o servidor deriva recurso e scope do principal e falha fechada quando Keycloak nega ou não consegue decidir. Limite mensagens, duração e concorrência, use TLS e evite registrar tokens, logs ou conteúdo de terminal.

⚠️ Atenção: o cliente JavaScript pode enviar bearer tokens em query string para WebSockets/SSE por limitação das APIs do browser. O relay BFF do Dokpod autentica o browser por cookie e acrescenta o Bearer apenas no salto server-side para a API.

Reconexão, fluxos e eventos que podem se perder

O cliente JavaScript não reconecta por padrão. withAutomaticReconnect() usa atrasos de 0, 2, 10 e 30 segundos; após quatro falhas, para e chama onclose. A política não cobre falhas do start() inicial, que precisam de estratégia própria. onreconnecting, onreconnected e onclose representam estados distintos.

Após reconectar, o servidor vê uma nova conexão. ConsoleRealtimeService guarda os ambientes observados e chama JoinEnvironmentAsync novamente. Atualizar apenas o indicador para “connected” deixa a interface sem receber invalidações se o cliente não restaurar a associação ao grupo.

connection.onreconnecting(() => this.status.set('connecting'));
connection.onreconnected(() => {
  const environmentIds = [...this.joined];
  this.joined.clear();
  this.status.set('connected');
  void this.watch(environmentIds);
});
connection.onclose(() => this.status.set('disconnected'));
Enter fullscreen mode Exit fullscreen mode

📂 Código-fonte: console-realtime.service.ts.

Reconexão não recupera mensagens emitidas durante a queda. Para inventário, faça refresh REST após reconectar e use SignalR para mudanças seguintes. Histórico, replay ou confirmação exigem sequência durável e retomada explícita, não são garantias do Hub.

Essa estratégia funciona melhor quando a consulta é idempotente e retorna uma visão consistente do recurso, em vez de depender de reaplicar cada notificação perdida.

O Hub também oferece fluxos longos. StreamContainerLogs autoriza container:logs, dura no máximo 15 minutos, propaga cancelamento e reavalia o acesso a cada 60 segundos. OpenTerminal recebe e devolve streams, exige WebSocket e aplica limites próprios. Esses recursos transportam conteúdo sob demanda e não devem herdar as garantias de uma invalidação.

Escala e operação sem prometer o que ainda não existe

Uma conexão SignalR consome socket, memória e keep-alive. O Dokpod configura 15 segundos para KeepAliveInterval e 30 para ClientTimeoutInterval, proporção recomendada pela documentação do ASP.NET Core. São valores do projeto: ajuste após medir proxy, latência e custo por conexão.

O proxy precisa encaminhar o upgrade WebSocket, evitar buffering que atrase SSE e alinhar timeouts aos pings. No Dokpod, o browser passa por nginx e BFF; um teste direto ao Kestrel não valida essa borda.

Com múltiplos processos, as requisições de uma conexão normalmente exigem afinidade. Há exceções documentadas, como instância única, Azure SignalR Service e WebSockets exclusivos com negociação ignorada. Redis backplane encaminha mensagens entre servidores, mas geralmente não remove a afinidade nem define ownership de sessões de agente ou roteamento de comandos.

Azure SignalR Service e Redis são opções com topologias, custos e dependências diferentes; o Dokpod não usa nenhuma delas. O plano começa com uma instância de API. Escala horizontal requer decisão sobre ownership, fencing e roteamento durável de comandos, motivada por métricas e ADR.

Observe conexões atuais e encerradas, timeouts, falhas de autorização e latência até o refresh. O ASP.NET Core oferece logs e métricas de conexão. Aumente a verbosidade somente durante diagnóstico: logs, HAR e traces podem conter payloads sensíveis.

Exemplo Prático: browser, BFF, Keycloak e invalidação

O fluxo abaixo usa o console do Dokpod: SignalR não substitui REST nem é o canal do agente.

  • O Angular registra handlers, solicita antiforgery ao BFF e conecta-se por /hubs/control-plane com o cookie de sessão.
  • O BFF valida sessão e origem e acrescenta o Bearer server-side no salto até a API.
  • JoinEnvironmentAsync valida o GUID e consulta Keycloak para environment:read naquele recurso.
  • Somente uma decisão permitida adiciona a conexão ao grupo determinístico do ambiente.
  • A API emite invalidated; o Angular consulta REST para obter o estado atual.
  • Após reconectar, o cliente reinscreve ambientes e atualiza o estado por REST; avisos do intervalo podem ter sido perdidos.
builder.Services.AddSignalR(options =>
{
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
});

app.MapHub<ControlPlaneHub>("/hubs/control-plane")
    .RequireAuthorization();
Enter fullscreen mode Exit fullscreen mode

📂 Código-fonte: ApiHost.cs. A API atual também configura autenticação JWT e uma política que exige usuário autenticado.

ConsoleRealtimeService é o cliente Angular e SignalRControlPlaneNotifier publica fora do Hub. Serviços notificam mudanças de inventário, eventos e hosts; a leitura seguinte continua usando uma rota HTTP autorizada. Agentes permanecem no canal gRPC/mTLS.

O relatório de prontidão de 25/09 registra conexão browser autorizada e negação do ambiente sem acesso; testes também cobrem rotas protegidas, grupo estável e identificador inválido. Isso valida uma fatia, não carga nem release: o veredito geral continua NO-GO, com gates de operação e distribuição pendentes. docs/bff.md preserva status histórico de 13/09, anterior à autorização horizontal; considerei o código e a avaliação mais recentes sem alterar esse registro.

SignalR, gRPC, SSE e WebSockets: escolha pelo fluxo

Use SignalR quando a aplicação ASP.NET Core se beneficia de Hub, grupos e clientes suportados. Para eventos unidirecionais, SSE pode ser menor; para controlar um canal duplex, WebSockets puros; para serviços com contrato explícito e streaming, gRPC.

Em todos os casos, avalie autenticação, observabilidade, semântica de falha e infraestrutura junto com o formato do tráfego.

Tecnologia Consumidor comum Fluxo O que oferece Trade-off principal
SignalR Browser e clientes ASP.NET Mensagens e streams nos dois sentidos Hub, grupos, negociação de transporte e integração .NET Conexões persistentes, autorização e estratégia de scale-out
SSE Browser Servidor → cliente Stream HTTP unidirecional simples Cliente envia comandos por outro canal; proxy não pode acumular o stream
WebSockets Browser ou serviço Duplex Canal persistente de baixo nível A aplicação administra framing, eventos, reconexão e roteamento
gRPC Serviços e agentes Unary ou streaming Contratos Protobuf, clientes gerados e HTTP/2 Integração com browser e infraestrutura têm requisitos próprios

No Dokpod, REST/OpenAPI atende consultas, SignalR entrega invalidações ao browser e gRPC/mTLS conecta a API aos agentes. O comparativo de tempo real e o guia de gRPC no .NET 10 detalham essas escolhas.

Dicas e Boas Práticas

  • Não mantenha estado de domínio na instância do Hub. Trate o Hub como uma fronteira de invocação e delegue regras para serviços de aplicação. Para estado que precisa sobreviver à desconexão, persista ou reconstrua por uma API durável.

  • Autorize antes de adicionar a conexão a um grupo. Derive o recurso e o scope no servidor, a partir do principal autenticado, e não confie em um nome de grupo fornecido pelo cliente. Reavalie permissões em fluxos longos quando a política exigir revogação durante a sessão.

  • Trate start() separado de reconexão automática. Configure o tratamento de falhas da primeira conexão e apresente os estados connecting, connected, reconnecting e disconnected. A política padrão de withAutomaticReconnect() tem tentativas finitas; não significa reconexão eterna.

  • Restaure assinaturas e estado após reconectar. Refaça a entrada em grupos porque a conexão nova não herda toda a inscrição anterior. Faça também uma consulta REST para obter o estado atual que pode ter mudado enquanto o cliente estava offline.

  • Envie invalidações pequenas, não snapshots grandes. O browser pode atualizar a projeção consultando o recurso autorizado. Para histórico, replay ou consumo confirmado, use um mecanismo durável com cursor e semântica explícita.

  • Mantenha a origem e a borda restritas. Prefira same-origin pelo BFF quando houver tokens server-side, valide Origin em conexões por cookie e não abra CORS com credenciais para qualquer origem. Configure nginx/balanceador para o transporte realmente habilitado.

  • Meça conexões antes de adicionar infraestrutura. Observe conexões ativas, duração, timeouts, reconexões, limites e tráfego por cliente. Afinidade, backplane ou serviço gerenciado resolvem problemas distintos; escolha depois de conhecer a topologia e a carga.

Conclusão

SignalR oferece uma API produtiva para comunicação em tempo real no ASP.NET Core, mas suas abstrações não eliminam as decisões de segurança e operação. Um Hub organiza chamadas; um grupo direciona mensagens; uma conexão pode cair; e a notificação não se transforma em estado persistente. Quando a aplicação precisa recuperar informação confiável, a consulta deve voltar a uma fonte durável.

O fluxo do Dokpod demonstra essa separação: o browser conecta pelo BFF, a API autoriza cada ambiente com Keycloak, envia uma invalidação e o Angular consulta REST. Reconexão restaura inscrições; gRPC/mTLS continua sendo o canal dos agentes.

Eu adotaria SignalR quando baixa latência, comunicação bidirecional ou integração com Hub compensarem o custo de conexões persistentes. Para stream unidirecional, compararia SSE; para serviços controlados, gRPC. A série percorre REST, GraphQL, gRPC e SignalR conforme a necessidade.

Antes de tratar esse canal como infraestrutura crítica, eu testaria a jornada pela borda real: autorização permitida e negada, reinício da API, perda de conexão, reinscrição e reconciliação pela consulta REST. O resultado precisa ser medido sob a carga esperada, não inferido de um teste local de Hub.

Resumo Objetivo

  • ASP.NET Core SignalR — biblioteca de tempo real com Hubs e negociação entre WebSockets, SSE e Long Polling compatíveis.
  • Hub do SignalR — expõe métodos e eventos; sua instância não deve guardar estado de domínio durável.
  • Grupos SignalR — direcionam mensagens, mas não são armazenamento, fila durável nem autorização.
  • Reconexão JavaScript — withAutomaticReconnect() tenta em 0, 2, 10 e 30 segundos; falhas de start() exigem tratamento separado.
  • Autorização por ambiente — JoinEnvironmentAsync consulta Keycloak para environment:read antes de associar a conexão.
  • Notificações do Dokpod — invalidated leva o browser à API REST, fonte do estado durável.
  • Escala SignalR — múltiplas instâncias exigem afinidade ou scale-out compatível; Redis e Azure SignalR têm requisitos distintos.
  • Prontidão do Dokpod — a validação SignalR permitida/negada não altera o veredito geral de release: NO-GO.

Leia Também

  • gRPC sem mistério: o que é e quando usar no .NET 10 — parte 1 da série e canal entre API e agentes.
  • BFF Backend For Frontend: Segurança em SPAs — cookies HttpOnly, antiforgery e proteção dos tokens no browser.
  • Tempo real no Angular e .NET: WebSockets, SSE e SignalR — comparativo detalhado dos transportes.
  • Arquitetura de Eventos sem Caos com C#: Guia Prático — eventos, consistência e processamento além do transporte.
  • Design de API REST: verbos HTTP e parameter binding — APIs consultadas depois de uma invalidação.

Referências

  • Visão geral do ASP.NET Core SignalR — conceitos, transportes e protocolo de Hub.
  • Hubs no ASP.NET Core SignalR — ciclo de vida, chamadas e uso de IHubContext.
  • Cliente JavaScript do SignalR — conexão, invocação, eventos e reconexão.
  • Autenticação e autorização no SignalR — cookies, bearer tokens, [Authorize] e autorização por método.
  • Grupos no SignalR — inscrição, roteamento e limites de segurança dos grupos.
  • Escala e hospedagem do SignalR — afinidade, Redis backplane, Azure SignalR Service e proxy nginx.
  • Configuração do SignalR — timeouts, keep-alive, limites e protocolos de serialização.
  • Diagnóstico do SignalR — logs, métricas, tracing e riscos de capturar payloads.
  • Repositório público do Dokpod — código consultado na branch main, sem uma release ou tag estável identificada. 📬

👉 Artigo completo com todos os exemplos de código: SignalR no .NET: do primeiro Hub à produção

Top comments (0)