SignalR | Dia a Dia | Ambiente .NET
- SignalR | Dia a Dia | Ambiente .NET
1. Modelo mental correto do SignalR
Pensa em SignalR assim:
- Hub = endpoint lógico RPC bidirecional
- Cliente = conexão persistente que registra handlers locais e invoca métodos remotos
- Transporte = WebSockets, SSE ou Long Polling
- Hub protocol = JSON ou MessagePack
- Escala = instância única, Redis backplane ou Azure SignalR Service
O erro mais comum é tratar o Hub como se fosse um singleton stateful. Não é. Cada chamada de método do hub roda em uma nova instância, então você não deve guardar estado em propriedade do hub. A própria Microsoft reforça isso, junto com duas outras regras críticas: não instanciar hub via DI diretamente e sempre usar await em chamadas assíncronas dependentes da vida útil do hub. (Microsoft Learn)
2. Pacotes principais
Server side
Em app ASP.NET Core moderno, o lado servidor já vem dentro do stack web. O ponto de entrada é:
using Microsoft.AspNetCore.SignalR;
Cliente .NET
Para cliente C# externo, console, desktop, worker etc.:
dotnet add package Microsoft.AspNetCore.SignalR.Client
3. Estrutura mínima do servidor
Program.cs
using Microsoft.AspNetCore.SignalR;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
WebApplication app = builder.Build();
app.MapHub<ChatHub>("/hub/chat");
app.Run();
Hub básico
using Microsoft.AspNetCore.SignalR;
public sealed class ChatHub : Hub
{
public async Task EnviarMensagem(string usuario, string mensagem)
{
await Clients.All.SendAsync("ReceberMensagem", usuario, mensagem);
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
await base.OnDisconnectedAsync(exception);
}
}
Esse é o núcleo: o cliente chama métodos públicos do hub, e o hub chama métodos registrados no cliente.
4. Hub fortemente tipado
Esse é um dos recursos mais importantes para C#. Em vez de string solta em SendAsync, você expõe uma interface do lado cliente e o hub passa a chamar métodos com tipagem forte. Isso reduz erro de nome, melhora refactor e IntelliSense. O material que você tem aí destaca isso claramente.
public interface IChatClient
{
Task ReceberMensagem(string usuario, string mensagem);
Task AtualizarStatus(string status);
}
public sealed class ChatHub : Hub<IChatClient>
{
public async Task EnviarMensagem(string usuario, string mensagem)
{
await Clients.All.ReceberMensagem(usuario, mensagem);
}
}
Quando você usa Hub<TClient>, também pode injetar IHubContext<ChatHub, IChatClient> fora do hub. A documentação oficial recomenda exatamente isso para HubContext fortemente tipado. (Microsoft Learn)
5. Objetos principais dentro do Hub
Dentro de Hub, você trabalha principalmente com:
ClientsContextGroups
Clients
Permite enviar mensagem para:
- todos:
Clients.All - caller:
Clients.Caller - outros exceto caller:
Clients.Others - connectionId específico:
Clients.Client(connectionId) - múltiplos clientes:
Clients.Clients(ids) - grupo:
Clients.Group("grupo") - múltiplos grupos:
Clients.Groups(...) - usuário:
Clients.User(userId) - múltiplos usuários:
Clients.Users(...)
Context
Traz metadados da conexão:
Context.ConnectionIdContext.UserContext.UserIdentifierContext.ItemsContext.GetHttpContext()Context.ConnectionAborted
Groups
Gerencia associação dinâmica:
await Groups.AddToGroupAsync(Context.ConnectionId, "contrato-123");
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "contrato-123");
Grupo é conceito lógico em memória distribuída do SignalR. Ele é ótimo para fan-out por tenant, contrato, sala, canal, CNPJ etc.
6. Envio fora do Hub com IHubContext
Essa é a forma correta quando quem dispara a mensagem não está dentro do hub: controller, minimal API, background service, consumer RabbitMQ, job, listener de evento etc. A Microsoft orienta usar IHubContext para isso. (Microsoft Learn)
using Microsoft.AspNetCore.SignalR;
public sealed class NotificacaoService
{
private readonly IHubContext<ChatHub, IChatClient> _hubContext;
public NotificacaoService(IHubContext<ChatHub, IChatClient> hubContext)
{
_hubContext = hubContext;
}
public Task NotificarGrupoAsync(string grupo, string mensagem)
{
return _hubContext.Clients.Group(grupo)
.ReceberMensagem("sistema", mensagem);
}
}
Esse modelo encaixa muito bem no teu cenário de API recebendo comando e empurrando para clients desktop.
7. Cliente .NET em C#
Esse é o ponto que muita gente conhece mal.
Conexão básica
using Microsoft.AspNetCore.SignalR.Client;
HubConnection connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/hub/chat")
.Build();
Registrar handlers antes do StartAsync
connection.On<string, string>("ReceberMensagem", (usuario, mensagem) =>
{
Console.WriteLine($"{usuario}: {mensagem}");
});
Abrir conexão
await connection.StartAsync();
Invocar método do servidor
Use InvokeAsync quando o método do hub retorna algo:
string resposta = await connection.InvokeAsync<string>("Ping");
Use SendAsync quando você só quer disparar e não precisa de retorno:
await connection.SendAsync("EnviarMensagem", "adolfo", "olá");
8. Eventos de ciclo de vida do cliente .NET
Você precisa dominar estes três:
connection.Reconnecting += error =>
{
Console.WriteLine("Reconectando...");
return Task.CompletedTask;
};
connection.Reconnected += connectionId =>
{
Console.WriteLine($"Reconectado. Novo ConnectionId: {connectionId}");
return Task.CompletedTask;
};
connection.Closed += error =>
{
Console.WriteLine("Conexão fechada.");
return Task.CompletedTask;
};
Por padrão, o cliente não reconecta automaticamente. Para isso, você precisa configurar WithAutomaticReconnect. A documentação oficial deixa isso explícito. (Microsoft Learn)
Reconexão automática
HubConnection connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/hub/chat")
.WithAutomaticReconnect()
.Build();
O retry padrão é: 0, 2, 10 e 30 segundos, até quatro tentativas. Isso vem da API oficial. (Microsoft Learn)
Você também pode customizar:
.WithAutomaticReconnect(new[]
{
TimeSpan.Zero,
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(15)
})
9. Streaming
SignalR suporta dois sentidos:
- server-to-client streaming
- client-to-server streaming
Isso é peça central quando você quer mandar muitos itens progressivamente, sem esperar o lote inteiro.
A documentação oficial informa que StreamAsync e StreamAsChannelAsync no cliente .NET são usados para invocar métodos de streaming do servidor para o cliente. (Microsoft Learn)
9.1 Server-to-client com IAsyncEnumerable
Hub
public sealed class StreamHub : Hub
{
public async IAsyncEnumerable<int> Contar(
int inicio,
int quantidade,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
for (int i = 0; i < quantidade; i++)
{
cancellationToken.ThrowIfCancellationRequested();
yield return inicio + i;
await Task.Delay(500, cancellationToken);
}
}
}
Cliente
await foreach (int valor in connection.StreamAsync<int>("Contar", 10, 5))
{
Console.WriteLine(valor);
}
9.2 Client-to-server com ChannelReader<T>
Hub
using System.Threading.Channels;
public sealed class UploadHub : Hub
{
public async Task ReceberLote(ChannelReader<string> stream)
{
await foreach (string item in stream.ReadAllAsync())
{
Console.WriteLine($"Recebido: {item}");
}
}
}
Cliente
Channel<string> channel = Channel.CreateUnbounded<string>();
_ = Task.Run(async () =>
{
await channel.Writer.WriteAsync("A");
await channel.Writer.WriteAsync("B");
channel.Writer.Complete();
});
await connection.SendAsync("ReceberLote", channel.Reader);
Na documentação atual, o cliente .NET também pode enviar IAsyncEnumerable<T> para métodos client-to-server streaming. (Microsoft Learn)
10. Configurações importantes do servidor
Aqui está o bloco que separa projeto hobby de projeto sério.
Configuração base
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = false;
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.HandshakeTimeout = TimeSpan.FromSeconds(15);
options.MaximumReceiveMessageSize = 32 * 1024;
options.StreamBufferCapacity = 10;
options.MaximumParallelInvocationsPerClient = 1;
});
O que cada uma faz
EnableDetailedErrors: padrãofalse; setrue, devolve erro detalhado ao cliente. Útil em dev, perigoso em prod porque pode expor informação sensível. (Microsoft Learn)HandshakeTimeout: padrão15s; se o handshake inicial não chegar nesse intervalo, a conexão fecha. Só altere se houver latência severa. (Microsoft Learn)KeepAliveInterval: padrão15s; envia ping automático para manter a conexão aberta. Ao alterar isso, oServerTimeoutdo cliente deve ser pelo menos o dobro. (Microsoft Learn)MaximumReceiveMessageSize: padrão32 KB; aumenta o limite da mensagem de entrada, mas também aumenta risco de DoS. (Microsoft Learn)StreamBufferCapacity: padrão10; quantidade máxima de itens bufferizados para upload stream do cliente. (Microsoft Learn)MaximumParallelInvocationsPerClient: padrão1; controla quantas invocações paralelas cada cliente pode fazer antes de enfileirar. (Microsoft Learn)
Configuração por Hub específico
builder.Services.AddSignalR()
.AddHubOptions<ChatHub>(options =>
{
options.EnableDetailedErrors = true;
});
Isso é útil quando um hub é “interno” e outro é exposto para internet.
11. Configurações do transporte HTTP
No despacho HTTP do hub, você também pode afinar o transporte.
app.MapHub<ChatHub>("/hub/chat", options =>
{
options.Transports =
HttpTransportType.WebSockets |
HttpTransportType.LongPolling;
options.LongPolling.PollTimeout = TimeSpan.FromSeconds(60);
options.ApplicationMaxBufferSize = 1024 * 1024;
options.TransportMaxBufferSize = 1024 * 1024;
});
Pontos principais
PollTimeout: padrão90s; controla quanto tempo o servidor segura uma requisição de long polling antes de responder. (Microsoft Learn)ApplicationMaxBufferSize: controla quantos bytes recebidos do servidor o cliente bufferiza antes de backpressure. (Microsoft Learn)TransportMaxBufferSize: controla quantos bytes enviados pelo app o servidor bufferiza antes de backpressure. (Microsoft Learn)
12. JSON x MessagePack
SignalR suporta os dois oficialmente. A documentação atual afirma isso de forma direta. (Microsoft Learn)
JSON
Mais simples, legível, melhor para debug, integração e compatibilidade.
builder.Services.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions.PropertyNamingPolicy = null;
options.PayloadSerializerOptions.WriteIndented = false;
});
MessagePack
Menor payload e melhor eficiência, mas mais r ígido. O material do teu acervo destaca a desvantagem prática: sensibilidade a nomes/casing e necessidade de pacote adicional.
dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack
builder.Services.AddSignalR()
.AddMessagePackProtocol();
Regra prática:
- usa JSON por padrão
- usa MessagePack quando payload, frequência e custo de banda realmente importam
13. Autenticação e autorização
SignalR integra com a autenticação do ASP.NET Core. Cada conexão pode ser associada a um usuário, acessível por Context.User, e um mesmo usuário pode ter várias conexões simultâneas. (Microsoft Learn)
Proteger o hub
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
[Authorize]
public sealed class SecureHub : Hub
{
}
Ou por policy:
[Authorize(Policy = "DesktopClientsOnly")]
public sealed class SecureHub : Hub
{
}
Cliente .NET com bearer token
A documentação oficial mostra AccessTokenProvider como mecanismo para fornecer token. Ela também observa que essa função é chamada antes de cada requisição HTTP feita pelo SignalR, o que é importante para renovação do token. (Microsoft Learn)
HubConnection connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/hub/secure", options =>
{
options.AccessTokenProvider = () => Task.FromResult("SEU_JWT");
})
.Build();
Cuidado com query string e navegador
Quando o cliente usa WebSockets ou SSE no navegador, o token pode ir na query string. A Microsoft recomenda sempre usar HTTPS e alerta para risco de logar URL com token. (Microsoft Learn)
14. Usuários, múltiplas conexões e IUserIdProvider
Quando você chama:
await Clients.User("123").SendAsync("ReceberMensagem", "oi");
o SignalR envia para todas as conexões daquele usuário. Isso é ótimo para cenários tipo:
- usuário logado em desktop + browser
- múltiplas janelas
- vários dispositivos
Se o identificador padrão não servir, você pode customizar com IUserIdProvider, por exemplo para usar cnpj, sub, nameidentifier ou outra claim.
15. Escala horizontal
Redis backplane
Serve para múltiplas instâncias trocarem mensagens e parecerem um único hub lógico. O teu material também cobre isso.
Configuração oficial:
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
builder.Services.AddSignalR()
.AddStackExchangeRedis("host:6379", options =>
{
options.Configuration.ChannelPrefix = RedisChannel.Literal("meu-app");
});
A Microsoft recomenda Redis backplane em produção somente se Redis e app estiverem no mesmo datacenter; caso contrário, latência degrada desempenho. Se estiver em Azure, a recomendação é Azure SignalR Service. (Microsoft Learn)
Sticky session
Com Redis backplane, normalmente você precisa de afinidade de sessão, exceto em um caso específico: todos os clientes usando apenas WebSockets e SkipNegotiation. A documentação oficial fala isso claramente. (Microsoft Learn)
Azure SignalR Service
Quando o volume de conexões é alto, ele costuma ser a opção mais limpa. A própria Microsoft destaca que, com Azure SignalR, sticky session não é necessária e os servidores da aplicação deixam de sustentar todas as conexões cliente diretamente. (Microsoft Learn)
16. Armadilhas clássicas
1. Guardar estado no hub
Errado. Cada invocação cria nova instância. (Microsoft Learn)
2. Não usar await em Clients.*
Pode falhar porque o método do hub termina antes do envio concluir. (Microsoft Learn)
3. Aumentar MaximumReceiveMessageSize sem critério
Isso aumenta superfície para abuso e consumo de memória. (Microsoft Learn)
4. Usar EnableDetailedErrors = true em produção
Pode vazar detalhes internos. (Microsoft Learn)
5. Não casar KeepAliveInterval do servidor com ServerTimeout do cliente
Se você mexe em um e esquece o outro, cria desconexão falsa. (Microsoft Learn)
6. Achar que grupo é banco de dados
Grupo é mecanismo de roteamento, não camada de persistência.
7. Confundir SendAsync com InvokeAsync
SendAsync: sem retornoInvokeAsync<T>: espera retorno tipado
17. Exemplo completo e limpo: servidor + cliente .NET
Servidor
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = false;
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.HandshakeTimeout = TimeSpan.FromSeconds(15);
options.MaximumReceiveMessageSize = 32 * 1024;
options.StreamBufferCapacity = 10;
});
WebApplication app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<ComandoHub>("/hub/comandos");
app.Run();
public interface IComandoClient
{
Task ReceberComando(string requestId, string payload);
Task ReceberStatus(string status);
}
[Authorize]
public sealed class ComandoHub : Hub<IComandoClient>
{
public override async Task OnConnectedAsync()
{
string? contrato = Context.User?.FindFirst("cnpj")?.Value;
if (!string.IsNullOrWhiteSpace(contrato))
await Groups.AddToGroupAsync(Context.ConnectionId, contrato);
await base.OnConnectedAsync();
}
public Task Ping() => Clients.Caller.ReceberStatus("pong");
public Task EnviarParaGrupo(string grupo, string requestId, string payload)
=> Clients.Group(grupo).ReceberComando(requestId, payload);
}
Cliente .NET
using Microsoft.AspNetCore.SignalR.Client;
HubConnection connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/hub/comandos", options =>
{
options.AccessTokenProvider = () => Task.FromResult("SEU_JWT");
})
.WithAutomaticReconnect()
.Build();
connection.On<string, string>("ReceberComando", async (requestId, payload) =>
{
Console.WriteLine($"Comando recebido: {requestId} - {payload}");
await Task.CompletedTask;
});
connection.On<string>("ReceberStatus", status =>
{
Console.WriteLine($"Status: {status}");
});
connection.Reconnecting += error =>
{
Console.WriteLine("Reconectando...");
return Task.CompletedTask;
};
connection.Reconnected += id =>
{
Console.WriteLine($"Reconectado com connectionId {id}");
return Task.CompletedTask;
};
connection.Closed += error =>
{
Console.WriteLine("Conexão fechada.");
return Task.CompletedTask;
};
await connection.StartAsync();
Console.WriteLine("Conectado.");
await connection.InvokeAsync("Ping");
Console.ReadLine();
await connection.DisposeAsync();
18. O que eu considero o conjunto mínimo que você realmente precisa dominar
Se você quiser ficar forte de verdade em SignalR com C#, foque em dominar estes blocos:
- ciclo de vida da conexão
- diferenças entre hub,
IHubContext, grupo e usuário - JSON vs MessagePack
- keep-alive, timeout e handshake
- autenticação JWT com
AccessTokenProvider - reconexão automática
- streaming com
IAsyncEnumerableeChannelReader - scale-out com Redis ou Azure SignalR
- restrições de produção: sticky session, afinidade, proxy, HTTPS e buffers
19. Testes de Unidade | SignalR
using Microsoft.AspNetCore.SignalR;
using Moq;
using Xunit;
public class ChatHubTests
{
[Fact]
public async Task EnviarMensagem_DeveEnviarParaTodos()
{
var clientProxy = new Mock<IClientProxy>();
var clients = new Mock<IHubCallerClients>();
clients.Setup(x => x.All).Returns(clientProxy.Object);
var hub = new ChatHub
{
Clients = clients.Object
};
await hub.EnviarMensagem("adolfo", "oi");
clientProxy.Verify(
x => x.SendCoreAsync(
"ReceberMensagem",
It.Is<object[]>(o =>
(string)o[0] == "adolfo" &&
(string)o[1] == "oi"),
default),
Times.Once);
}
}
public class ChatHub : Hub
{
public Task EnviarMensagem(string usuario, string mensagem)
=> Clients.All.SendAsync("ReceberMensagem", usuario, mensagem);
}