SignalR abstrai WebSockets, Server-Sent Events e Long Polling numa API unificada, deixando o servidor escolher automaticamente o melhor transporte disponível. O resultado: comunicação bidirecional em tempo real com poucas linhas de código — notificações ao vivo, dashboards atualizados automaticamente, chat, colaboração em tempo real.
Setup e primeiro Hub
dotnet add package Microsoft.AspNetCore.SignalR.Client // cliente .NET (opcional) // SignalR server já vem no pacote ASP.NET Core// Program.cs: builder.Services.AddSignalR(options => { options.EnableDetailedErrors = builder.Environment.IsDevelopment(); options.KeepAliveInterval = TimeSpan.FromSeconds(15); options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); options.MaximumReceiveMessageSize = 32 * 1024; // 32KB por mensagem });
// Com Redis para scale-out (múltiplas instâncias): builder.Services.AddSignalR() .AddStackExchangeRedis(builder.Configuration[“Redis:ConnectionString”]!, options => { options.Configuration.ChannelPrefix = RedisChannel.Literal(“NeryxApp”); });
// Mapeando Hubs: app.MapHub<NotificacoesHub>(“/hubs/notificacoes”); app.MapHub<DashboardHub>(“/hubs/dashboard”); app.MapHub<ChatHub>(“/hubs/chat”);
Hub de notificações com autenticação JWT
// Hub com autenticação — apenas usuários autenticados se conectam:
[Authorize]
public class NotificacoesHub : Hub
{
private readonly ILogger<NotificacoesHub> _logger;
public NotificacoesHub(ILogger<NotificacoesHub> logger)
=> _logger = logger;
// Chamado quando o cliente se conecta
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier!; // vem do claim "sub" do JWT
// Adiciona ao grupo pessoal do usuário (para notificações individuais)
await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{userId}");
// Adiciona ao grupo da empresa (para notificações de tenant)
var tenantId = Context.User!.FindFirst("tenant_id")?.Value;
if (tenantId != null)
await Groups.AddToGroupAsync(Context.ConnectionId, $"tenant:{tenantId}");
_logger.LogInformation("Usuário {UserId} conectado. ConnectionId: {ConnId}",
userId, Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation("Usuário {UserId} desconectado", Context.UserIdentifier);
await base.OnDisconnectedAsync(exception);
}
// Método invocável pelo cliente (client → server)
public async Task MarcarNotificacaoComoLida(Guid notificacaoId)
{
var userId = Context.UserIdentifier!;
// ... lógica de negócio
// Confirma de volta para o cliente
await Clients.Caller.SendAsync("NotificacaoMarcada", notificacaoId);
}
}
Enviando notificações de qualquer lugar via IHubContext
O grande diferencial do SignalR é poder enviar mensagens a partir de qualquer serviço — não apenas dentro do Hub:
// Serviço que envia notificações em tempo real: public class NotificacaoService { private readonly IHubContext<NotificacoesHub> _hub; private readonly AppDbContext _db;public NotificacaoService(IHubContext<NotificacoesHub> hub, AppDbContext db) { _hub = hub; _db = db; } // Notifica um usuário específico (qualquer dispositivo conectado) public async Task NotificarUsuarioAsync(Guid userId, NotificacaoDto notificacao) { // Persiste no banco _db.Notificacoes.Add(new Notificacao { UserId = userId, ...notificacao }); await _db.SaveChangesAsync(); // Envia em tempo real para todos os dispositivos do usuário await _hub.Clients .Group($"user:{userId}") .SendAsync("NovaNotificacao", notificacao); } // Notifica todos de um tenant (broadcast para empresa) public async Task NotificarTenantAsync(Guid tenantId, string mensagem) { await _hub.Clients .Group($"tenant:{tenantId}") .SendAsync("AlertaTenant", new { mensagem, timestamp = DateTime.UtcNow }); } // Notifica todos os conectados (broadcast global — usar com cuidado) public async Task BroadcastSistemaAsync(string mensagem) { await _hub.Clients.All.SendAsync("MensagemSistema", new { mensagem }); }}
// Uso em um endpoint — após salvar pedido, notifica cliente: [HttpPost(“pedidos”)] public async Task<IActionResult> CriarPedido([FromBody] CriarPedidoRequest request) { var pedido = await _pedidoService.CriarAsync(request);
// Notificação em tempo real — sem polling! await _notificacaoService.NotificarUsuarioAsync( pedido.ClienteId, new NotificacaoDto("Pedido confirmado", $"Seu pedido #{pedido.Numero} foi recebido.")); return CreatedAtAction(nameof(GetPedido), new { id = pedido.Id }, null);
}
Dashboard de métricas ao vivo
// Hub para dashboard — sem autenticação de usuário, mas com política de role: [Authorize(Policy = "requer-admin")] public class DashboardHub : Hub { // Clientes se inscrevem em métricas específicas public async Task InscreverMetrica(string metricaNome) { await Groups.AddToGroupAsync(Context.ConnectionId, $"metrica:{metricaNome}"); await Clients.Caller.SendAsync("InscricaoConfirmada", metricaNome); }public async Task CancelarMetrica(string metricaNome) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"metrica:{metricaNome}"); }}
// Worker que publica métricas a cada segundo: public class MetricasBroadcastWorker : BackgroundService { private readonly IHubContext<DashboardHub> _hub; private readonly IMetricasService _metricas;
protected override async Task ExecuteAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { var pedidosAtivos = await _metricas.GetPedidosAtivosAsync(); var usuariosOnline = await _metricas.GetUsuariosOnlineAsync(); var receitaHora = await _metricas.GetReceitaUltimaHoraAsync(); // Envia para quem está inscrito na métrica "pedidos" await _hub.Clients .Group("metrica:pedidos") .SendAsync("AtualizacaoMetrica", new { pedidosAtivos, timestamp = DateTime.UtcNow }); // Envia para quem está inscrito na métrica "receita" await _hub.Clients .Group("metrica:receita") .SendAsync("AtualizacaoMetrica", new { receitaHora, usuariosOnline }); await Task.Delay(TimeSpan.FromSeconds(1), ct); } }
}
Cliente React com @microsoft/signalr
// npm install @microsoft/signalrimport { HubConnectionBuilder, LogLevel, HubConnectionState } from “@microsoft/signalr”; import { useEffect, useRef, useState } from “react”;
export function useNotificacoes(token: string) { const connectionRef = useRef<signalR.HubConnection | null>(null); const [notificacoes, setNotificacoes] = useState<Notificacao[]>([]); const [conectado, setConectado] = useState(false);
useEffect(() => { const connection = new HubConnectionBuilder() .withUrl("/hubs/notificacoes", { accessTokenFactory: () => token, // JWT injetado nos headers }) .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // backoff .configureLogging(LogLevel.Warning) .build(); // Ouve o evento do servidor connection.on("NovaNotificacao", (notificacao: Notificacao) => { setNotificacoes(prev => [notificacao, ...prev]); // Toca som, mostra toast, etc. }); connection.onreconnecting(() => setConectado(false)); connection.onreconnected(() => setConectado(true)); connection.start() .then(() => setConectado(true)) .catch(err => console.error("Erro ao conectar SignalR:", err)); connectionRef.current = connection; return () => { connection.stop(); }; }, [token]); // Invoca método no servidor const marcarComoLida = (id: string) => { connectionRef.current?.invoke("MarcarNotificacaoComoLida", id); }; return { notificacoes, conectado, marcarComoLida };
}
Scale-out: múltiplas instâncias com Redis Backplane
Por padrão, SignalR mantém conexões em memória — se você tem 3 instâncias da API, uma conexão de usuário existe em apenas uma delas. Para enviar mensagem a um usuário independente de qual instância recebeu o request, use o Redis Backplane:
// Já mostrado no setup, mas vale detalhar: builder.Services.AddSignalR() .AddStackExchangeRedis("seu-redis:6379,password=...", options => { // Prefixo para não colidir com outros dados no Redis options.Configuration.ChannelPrefix = RedisChannel.Literal("SignalR:NeryxApp"); });
// Com isso: IHubContext.Clients.Group(“user:123”).SendAsync(…) // O Redis publica o evento e TODAS as instâncias entregam para conexões que elas gerenciam. // Funciona transparentemente — zero mudança no código do Hub.
Typed Hubs: contrato fortemente tipado
// Define interface dos métodos que o SERVER envia para o CLIENT: public interface INotificacoesClient { Task NovaNotificacao(NotificacaoDto notificacao); Task NotificacaoMarcada(Guid notificacaoId); Task AlertaTenant(AlertaDto alerta); }// Hub com tipagem — erros de nome de método viram erros de compilação: public class NotificacoesHub : Hub<INotificacoesClient> { public override async Task OnConnectedAsync() { await Groups.AddToGroupAsync(Context.ConnectionId, $“user:{Context.UserIdentifier}”); await base.OnConnectedAsync(); } }
// IHubContext também fica tipado: public class NotificacaoService { private readonly IHubContext<NotificacoesHub, INotificacoesClient> _hub;
public async Task NotificarAsync(Guid userId, NotificacaoDto dto) { // Autocompletar e type-check no método: await _hub.Clients.Group($"user:{userId}").NovaNotificacao(dto); }
}
Quando usar SignalR vs polling vs SSE
Use SignalR quando o cliente também precisa enviar dados para o servidor (chat, colaboração, jogos). Use Server-Sent Events (SSE) quando o fluxo é somente servidor → cliente (notificações, progresso de upload, feeds). Use polling apenas como fallback para ambientes sem WebSocket. Para firehose de eventos de alta frequência (>10/s por conexão), avalie se SSE + HTTP/2 não é mais eficiente que SignalR.
Conclusão
SignalR resolve comunicação em tempo real em .NET com elegância: Hub tipado, grupos de conexão, autenticação JWT nativa e scale-out transparente com Redis. O padrão de notificações individuais via grupo user:{id} cobre 80% dos casos de uso em aplicações SaaS — de alertas a dashboards ao vivo.
Se você precisa adicionar tempo real a uma aplicação .NET existente — notificações, progresso de tarefas, colaboração ou dashboards — a Neryx pode projetar e implementar a solução certa. Consultoria inicial gratuita.
Leitura complementar: