Streaming com SignalR | WebSocket | SignalR | CSharp
- Envio contínuo de dados entre servidor e cliente
O que é streaming no SignalR
Streaming no SignalR é o envio de vários itens ao longo do tempo dentro de uma única chamada lógica.
Em vez disso:
await Clients.Caller.SendAsync("Mensagem", item1);
await Clients.Caller.SendAsync("Mensagem", item2);
await Clients.Caller.SendAsync("Mensagem", item3);
Você faz isso:
await foreach (var item in stream)
{
// recebe item por item
}
O próprio material de SignalR destaca que além de chamadas únicas, o SignalR permite streaming de dados do servidor para o cliente e do cliente para o servidor .
Tipos de streaming
| Tipo | Direção | Exemplo |
|---|---|---|
| Server streaming | Servidor → Cliente | API manda progresso para o desktop |
| Client streaming | Cliente → Servidor | Desktop envia vários registros para API |
| Bidirecional | Ambos | Um lado transmite e o outro responde |
Quando usar
Use streaming quando você precisa enviar dados aos poucos:
- progresso de processamento
- logs em tempo real
- leitura incremental de dados
- status de execução
- envio de vários itens sem abrir várias chamadas
- monitoramento em tempo real
- comandos longos com retorno parcial
Não use streaming para uma resposta simples. Para isso, use InvokeAsync ou SendAsync.
Exemplo 1: Server streaming com IAsyncEnumerable
Neste modelo, o cliente chama um método no Hub e o servidor vai retornando itens aos poucos.
DTO
public sealed class ProgressoComandoDto
{
public Guid RequestId { get; init; }
public int Percentual { get; init; }
public string Mensagem { get; init; } = string.Empty;
}
Hub
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.SignalR;
public sealed class ComandoHub : Hub
{
public async IAsyncEnumerable<ProgressoComandoDto> ExecutarComandoComStream(
string sql,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
Guid requestId = Guid.NewGuid();
for (int i = 0; i <= 100; i += 10)
{
cancellationToken.ThrowIfCancellationRequested();
yield return new ProgressoComandoDto
{
RequestId = requestId,
Percentual = i,
Mensagem = $"Processando comando: {i}%"
};
await Task.Delay(500, cancellationToken);
}
}
}
Cliente .NET consumindo stream
using Microsoft.AspNetCore.SignalR.Client;
var connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/hub/comandos")
.WithAutomaticReconnect()
.Build();
await connection.StartAsync();
var stream = connection.StreamAsync<ProgressoComandoDto>(
"ExecutarComandoComStream",
"SELECT * FROM PRODUTOS");
await foreach (var item in stream)
{
Console.WriteLine($"{item.Percentual}% - {item.Mensagem}");
}
Fluxo visual
Cliente chama:
ExecutarComandoComStream(sql)
Servidor responde:
10%
20%
30%
40%
...
100%
Exemplo 2: Server streaming usando ChannelReader
Esse modelo é útil quando você quer escrever no stream a partir de outra tarefa.
Hub
using System.Threading.Channels;
using Microsoft.AspNetCore.SignalR;
public sealed class ComandoHub : Hub
{
public ChannelReader<ProgressoComandoDto> ExecutarComandoComChannel(string sql)
{
var channel = Channel.CreateUnbounded<ProgressoComandoDto>();
Guid requestId = Guid.NewGuid();
_ = Task.Run(async () =>
{
try
{
for (int i = 0; i <= 100; i += 10)
{
await channel.Writer.WriteAsync(new ProgressoComandoDto
{
RequestId = requestId,
Percentual = i,
Mensagem = $"Executando: {i}%"
});
await Task.Delay(500);
}
}
catch (Exception ex)
{
channel.Writer.TryComplete(ex);
return;
}
channel.Writer.TryComplete();
});
return channel.Reader;
}
}
Cliente .NET
var stream = connection.StreamAsync<ProgressoComandoDto>(
"ExecutarComandoComChannel",
"SELECT * FROM CLIENTES");
await foreach (var item in stream)
{
Console.WriteLine($"{item.Percentual}% - {item.Mensagem}");
}
Diferença entre IAsyncEnumerable e ChannelReader
| Opção | Quando usar |
|---|---|
IAsyncEnumerable<T> | fluxo simples, linear, fácil de entender |
ChannelReader<T> | fluxo produzido por tarefa, fila interna ou eventos externos |
Para a maioria dos casos, comece com IAsyncEnumerable<T>.
Exemplo 3: Client streaming
Aqui o cliente envia vários itens para o servidor.
DTO
public sealed class RegistroClienteDto
{
public Guid RequestId { get; init; }
public string Conteudo { get; init; } = string.Empty;
}
Hub recebendo stream do cliente
using Microsoft.AspNetCore.SignalR;
public sealed class ComandoHub : Hub
{
public async Task ReceberRegistrosCliente(
IAsyncEnumerable<RegistroClienteDto> registros,
CancellationToken cancellationToken)
{
await foreach (var registro in registros.WithCancellation(cancellationToken))
{
Console.WriteLine($"Recebido: {registro.RequestId} - {registro.Conteudo}");
// Exemplo:
// await filaInterna.EnqueueAsync(registro, cancellationToken);
// await banco.GravarAsync(registro, cancellationToken);
}
}
}
Cliente enviando stream
using System.Threading.Channels;
using Microsoft.AspNetCore.SignalR.Client;
var channel = Channel.CreateUnbounded<RegistroClienteDto>();
await connection.SendAsync("ReceberRegistrosCliente", channel.Reader);
for (int i = 1; i <= 10; i++)
{
await channel.Writer.WriteAsync(new RegistroClienteDto
{
RequestId = Guid.NewGuid(),
Conteudo = $"Registro {i}"
});
await Task.Delay(300);
}
channel.Writer.Complete();
Exemplo 4: streaming com cancelamento
O cancelamento é essencial para não deixar processamento preso quando o cliente desconecta.
public async IAsyncEnumerable<ProgressoComandoDto> Monitorar(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
int contador = 0;
while (!cancellationToken.IsCancellationRequested)
{
yield return new ProgressoComandoDto
{
RequestId = Guid.NewGuid(),
Percentual = contador,
Mensagem = $"Evento {contador}"
};
contador++;
await Task.Delay(1000, cancellationToken);
}
}
Cuidados importantes
| Cuidado | Motivo |
|---|---|
Sempre use CancellationToken | evita stream infinito após desconexão |
| Evite stream sem limite | pode consumir memória e CPU |
| Não use para payload gigante único | prefira HTTP download/upload |
| Controle frequência de envio | evita pressão no WebSocket |
| Use MessagePack se tiver alto volume | reduz payload |
| Trate exceções no produtor | evita stream travado |
Streaming não é fila
SignalR streaming não substitui RabbitMQ.
| SignalR streaming | RabbitMQ |
|---|---|
| conexão ativa | mensagem persistente |
| tempo real | assíncrono/durável |
| cliente conectado | cliente pode estar offline |
| sem garantia durável | pode ter confirmação e persistência |
Para o seu cenário:
- SignalR streaming: progresso em tempo real para cliente conectado
- RabbitMQ: resposta durável, processamento assíncrono, retry e persistência
Exemplo aplicado ao seu cenário
Fluxo:
API recebe comando
↓
SignalR envia comando ao desktop
↓
desktop começa execução
↓
desktop envia progresso via stream
↓
API acompanha progresso
↓
resultado final pode ir para RabbitMQ
Melhor uso
Comando curto → SendAsync
Comando com progresso → Streaming
Resposta durável → RabbitMQ
Arquivo grande → HTTP upload/download