Programação Por Equipe DevBras

Capa do artigo: Notificações em tempo real com SignalR no ASP.NET Core que não se perdem

Notificações em tempo real com SignalR no ASP.NET Core que não se perdem

Grave a notificação no banco, avise pelo SignalR o usuário certo e recarregue a lista ao reconectar. Inclui hub, serviço, cliente JavaScript e proxy.

O gerente aprovou o reembolso às 14h02. A analista estava com o notebook fechado, numa reunião. Quando abriu a tela às 14h40, o sino estava zerado e ela só soube da aprovação por e-mail no dia seguinte. A resposta curta: o SignalR avisa que algo aconteceu; quem guarda o que aconteceu é o banco. Grave a notificação primeiro, envie pelo hub para o usuário certo e, toda vez que a conexão voltar, busque a lista pelo HTTP normal.

Este guia monta esse fluxo completo num ASP.NET Core: hub autenticado, serviço que grava e publica, cliente JavaScript com reconexão e a configuração de proxy que costuma derrubar tudo em produção.

Por que só empurrar a mensagem não basta

Uma conexão SignalR existe enquanto a aba está aberta e a rede colabora. Notebook fechado, celular no túnel, deploy reiniciando o servidor: em todos esses casos, a mensagem enviada naquele instante não tem para onde ir. O SignalR não guarda fila de mensagens para quem está desconectado.

Por isso a notificação tem duas vidas. Uma é o registro, uma linha na tabela com usuário, texto, data e se foi lida. A outra é o aviso, um empurrão pela conexão que faz o sino piscar sem recarregar a página. Se o aviso se perde, o registro continua lá. O post notificação em tempo real: o aviso é extra, o registro é que fica aprofunda esse princípio; aqui o foco é a implementação.

Como implementar passo a passo

  1. Crie a tabela de notificações e o endpoint HTTP que lista as do usuário logado.
  2. Registre o SignalR e mapeie o hub num caminho fixo.
  3. Exija autenticação no hub, para que o servidor saiba quem é cada conexão.
  4. No ponto em que o fato acontece, grave a notificação e só depois envie para o usuário.
  5. No cliente, conecte com reconexão automática e recarregue a lista ao reconectar.
  6. Configure o proxy para repassar WebSockets.

Hub e registro no Program.cs

// Hubs/NotificacoesHub.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

[Authorize]
public class NotificacoesHub : Hub
{
}

// Program.cs (trechos)
builder.Services.AddSignalR();
builder.Services.AddScoped<NotificacaoService>();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapHub<NotificacoesHub>("/hubs/notificacoes");

app.MapGet("/api/notificacoes", async (ClaimsPrincipal user, AppDbContext db) =>
{
    var id = user.FindFirstValue(ClaimTypes.NameIdentifier);
    return await db.Notificacoes
        .Where(n => n.UsuarioId == id)
        .OrderByDescending(n => n.CriadaEm)
        .Take(50)
        .ToListAsync();
}).RequireAuthorization();

O hub está vazio de propósito. Ele não calcula nada, não consulta pedido, não aplica regra. Sua única função aqui é aceitar conexões autenticadas. Por padrão, o SignalR identifica o usuário pela claim ClaimTypes.NameIdentifier, e é esse valor que Clients.User(...) usa para achar todas as conexões daquela pessoa, em todas as abas.

O serviço que grava e avisa

// Services/NotificacaoService.cs
using Microsoft.AspNetCore.SignalR;

public class NotificacaoService(AppDbContext db, IHubContext<NotificacoesHub> hub)
{
    public async Task NotificarAsync(string usuarioId, string texto)
    {
        var n = new Notificacao
        {
            UsuarioId = usuarioId,
            Texto = texto,
            CriadaEm = DateTime.UtcNow
        };
        db.Notificacoes.Add(n);
        await db.SaveChangesAsync();

        await hub.Clients.User(usuarioId)
            .SendAsync("novaNotificacao", new { n.Id, n.Texto, n.CriadaEm });
    }
}

A ordem importa: primeiro SaveChangesAsync, depois SendAsync. Se o envio falhar, o registro existe. Se fosse o contrário, a tela poderia mostrar um aviso que nunca foi gravado. No exemplo do reembolso, o código que aprova chama NotificarAsync(analistaId, "Reembolso #4821 aprovado") depois de salvar a aprovação.

Para avisar um setor inteiro, use grupos: no OnConnectedAsync do hub, chame Groups.AddToGroupAsync(Context.ConnectionId, "financeiro") com base nas claims do usuário, e publique com Clients.Group("financeiro"). Grupos não são persistentes; a conexão entra de novo a cada reconexão, e é justamente por isso que essa lógica fica no OnConnectedAsync.

O cliente JavaScript

import * as signalR from '@microsoft/signalr';

const conexao = new signalR.HubConnectionBuilder()
  .withUrl('/hubs/notificacoes')
  .withAutomaticReconnect()
  .build();

async function carregarLista() {
  const resp = await fetch('/api/notificacoes');
  if (resp.ok) desenharSino(await resp.json());
}

conexao.on('novaNotificacao', (n) => {
  adicionarNoTopo(n);
});

conexao.onreconnected(() => carregarLista());

conexao.onclose(() => {
  setTimeout(() => conexao.start().then(carregarLista).catch(() => {}), 30000);
});

await carregarLista();
await conexao.start();

desenharSino e adicionarNoTopo são as funções da sua interface. O detalhe que resolve o caso da analista é o onreconnected: a reconexão automática tenta de novo algumas vezes (por padrão, depois de 0, 2, 10 e 30 segundos) e, quando consegue, a lista é buscada no HTTP. Se todas as tentativas falharem, o onclose dispara e você decide quando tentar outra vez.

Quando a conexão não sobe em produção

No seu computador funciona; atrás do Nginx, a tela fica muda. Três causas respondem pela maior parte dos casos:

  • Caminho diferente. O cliente chama /hubs/notificacoes, mas a aplicação roda sob um subcaminho ou o proxy reescreve a URL. A negociação devolve 404. O post SignalR que devolve 404 na negociação mostra como diagnosticar.
  • Proxy sem WebSocket. O Nginx precisa repassar os cabeçalhos de upgrade, senão a conexão cai para transportes mais lentos ou falha.
  • Vários servidores sem backplane. Com duas instâncias, o usuário está conectado na A e a notificação é enviada pela B. Use o backplane Redis do SignalR ou o Azure SignalR Service, e sticky sessions no balanceador quando houver negociação.
location /hubs/ {
    proxy_pass http://127.0.0.1:5000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 120s;
}

Se a autenticação for por JWT em vez de cookie, lembre que o navegador não manda cabeçalho Authorization no WebSocket. O cliente usa accessTokenFactory e o token vai na query string; no servidor, você lê esse valor no evento OnMessageReceived do JwtBearer para os caminhos do hub.

Erros comuns

  • Enviar pelo hub sem gravar no banco. Quem estava offline perde o aviso para sempre.
  • Colocar regra de negócio dentro do hub. O hub vira um segundo controlador, difícil de testar e chamado por qualquer cliente conectado.
  • Usar Clients.All para avisos pessoais. Todo mundo recebe a notificação de reembolso da analista.
  • Não recarregar a lista ao reconectar. O minuto em que a rede caiu some da tela.
  • Proxy sem cabeçalhos de upgrade. A conexão falha ou fica presa num transporte lento, e o sintoma parece bug no código.
  • Escalar para duas instâncias sem backplane. Metade das notificações não chega, de forma aleatória.

Checklist

  • Toda notificação é gravada antes de ser enviada pelo hub.
  • Existe um endpoint HTTP que lista as notificações do usuário logado.
  • O hub exige autenticação e não contém regra de negócio.
  • Avisos pessoais usam Clients.User; avisos de setor usam grupos.
  • O cliente usa withAutomaticReconnect e recarrega a lista no onreconnected.
  • O proxy repassa Upgrade e Connection no caminho do hub.
  • Com mais de uma instância, há backplane configurado.

Faça o teste agora: abra o sistema, desligue o Wi-Fi, dispare uma notificação para você mesmo por outro dispositivo e ligue a rede de novo. O sino mostra o aviso sem você apertar F5?

Avalie este artigo

Referência: https://learn.microsoft.com/pt-br/aspnet/core/tutorials/signalr?view=aspnetcore-7.0&tabs=visual-studio