ASP.NET Minimal SignalR

O SignalR com minimal APIs mostra como o novo modelo de hospedagem do .NET 6 também simplifica a comunicação em tempo real. No artigo anterior, vimos as Minimal APIs e o novo template Web do ASP.NET, e agora vamos conferir as principais mudanças do SignalR nesse cenário.

Ao longo do texto, você vai criar um servidor com poucas linhas, expor um Hub que faz streaming de dados e consumir esse fluxo a partir de um Console Application.

Antes de começar

Se você ainda não viu o novo template do ASP.NET, vale ler primeiro o artigo sobre ASP.NET Minimal APIs. Ele explica o WebApplication, o builder e a ideia de concentrar tudo no Program.cs.

Além disso, você precisa do SDK do .NET 6 instalado na máquina. Todo o exemplo deste artigo usa a linha de comando, então qualquer editor serve.

O que é o SignalR com minimal APIs

O SignalR é a tecnologia da Microsoft que roda sobre o ASP.NET e encapsula chamadas ao WebSocket. Dessa forma, ele provê comunicação em tempo real entre o seu frontend (client) e o servidor.

Quando falamos em SignalR com minimal APIs, falamos da mesma biblioteca configurada no modelo enxuto do .NET 6. Ou seja, sem a classe Startup, sem ConfigureServices e sem UseEndpoints.

Segundo a documentação oficial do ASP.NET Core SignalR, a biblioteca é open source e permite que o código do servidor envie conteúdo aos clients instantaneamente. Por isso, ela aparece em dashboards, chats, jogos, notificações e aplicações colaborativas.

Por que usar SignalR com minimal APIs

Até o .NET 5, configurar o SignalR exigia uma classe Startup com dois métodos separados. Veja como era o código equivalente nessa época.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // registra os serviços do SignalR
        services.AddSignalR();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        // mapeia o Hub dentro do UseEndpoints
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<MyHub>("/chat");
        });
    }
}

Além dessa classe, o projeto ainda tinha um Program.cs com o CreateHostBuilder. Ou seja, eram dois arquivos e várias camadas de configuração para expor um único Hub.

Com o SignalR com minimal APIs, o mesmo resultado cabe em quatro linhas dentro do Program.cs. Por isso, o modelo novo reduz a cerimônia e deixa o foco no Hub, que é onde mora a regra de negócio.

Na prática, o SignalR com minimal APIs também facilita protótipos, demonstrações e microsserviços pequenos. Você cria, testa e descarta um servidor de tempo real em poucos minutos.

Como o SignalR funciona por baixo dos panos

Antes de escrever código, vale entender o que a biblioteca faz por você. Afinal, o SignalR é uma abstração sobre diferentes técnicas de transporte.

Transportes suportados

O SignalR suporta três transportes, nesta ordem de fallback: WebSockets, Server-Sent Events e Long Polling. Assim, a biblioteca escolhe automaticamente o melhor método disponível entre servidor e client.

O WebSockets é o transporte preferido, porque geralmente entrega a melhor performance. Isso vale tanto para o modelo antigo quanto para o SignalR com minimal APIs, já que o transporte não depende da forma de hospedagem.

Ainda sobre esse ponto, a conexão sempre começa com uma requisição HTTP de negociação. No entanto, se algum proxy ou navegador não suportar WebSockets, a conexão continua funcionando com as outras opções.

Hubs e protocolos

A comunicação acontece por meio de Hubs, que funcionam como um pipeline de alto nível. Com isso, o client chama métodos no servidor e o servidor chama métodos no client.

O SignalR traz dois protocolos nativos: um baseado em JSON, que é o padrão, e outro binário baseado em MessagePack. Em geral, o MessagePack gera mensagens menores que o JSON.

Criando o servidor de SignalR com minimal APIs

Agora vamos colocar a mão na massa. Primeiro, criamos o servidor que vai hospedar o Hub.

Iniciando o projeto

Vamos começar criando nosso servidor com o template web, que é o template vazio das Minimal APIs.

dotnet new web -o SignalServer
cd SignalServer
dotnet add package Microsoft.AspNetCore.SignalR

O comando dotnet new web gera um Program.cs mínimo, com apenas um endpoint de exemplo. Em seguida, entramos na pasta e adicionamos o pacote do SignalR.

O pacote do SignalR no servidor

O pacote Microsoft.AspNetCore.SignalR é tudo que precisamos para trabalhar com SignalR no servidor. Na prática, porém, desde o ASP.NET Core 3.0 as bibliotecas do servidor fazem parte do shared framework Microsoft.AspNetCore.App.

Em outras palavras, um projeto web do .NET 6 já enxerga as classes do SignalR mesmo sem esse comando. Portanto, a referência explícita ao pacote é opcional no servidor.

Criando um Hub

Diferente dos projetos ASP.NET MVC, o SignalR utiliza Hub em vez de controladores. Os Hubs recebem e distribuem mensagens em tempo real.

Por sua vez, um Hub nada mais é do que uma classe que herda da classe base Hub.

class MyHub : Hub
{
    // métodos públicos aqui ficam disponíveis para o client
}

Todo método público dessa classe pode ser chamado pelos clients conectados. Além disso, a biblioteca cuida da serialização dos parâmetros e dos retornos, inclusive de tipos complexos.

O ciclo de vida do Hub

Um detalhe importante: cada chamada de método roda em uma nova instância do Hub. Por isso, a documentação recomenda não guardar estado em propriedades da classe.

Essa regra continua igual no SignalR com minimal APIs, porque o modelo de hospedagem não altera o ciclo de vida do Hub.

Se você precisar enviar mensagens de fora do Hub, use o IHubContext. Da mesma forma, nunca instancie o Hub manualmente pela injeção de dependência.

Streaming no SignalR com minimal APIs

Se estamos falando em tempo real, em ficar conectado ao servidor, estamos falando sobre streaming de dados na maioria dos casos. Para isso, o C# incluiu nas últimas versões um tipo chamado IAsyncEnumerable.

Dessa forma, podemos especificar nosso retorno como um enumerador assíncrono. Assim, o servidor faz streaming dos dados enquanto houver conexões ativas nesse endpoint.

Esse é o recurso central do SignalR com minimal APIs que vamos explorar aqui.

public async IAsyncEnumerable<DateTime> Streaming(CancellationToken cancellationToken)
{
    while (true)
    {
        // envia a data e hora atual em UTC a cada segundo
        yield return DateTime.UtcNow;
        await Task.Delay(1000, cancellationToken);
    }
}

De acordo com a documentação de streaming do SignalR, um método do Hub vira um método de streaming quando retorna IAsyncEnumerable<T> ou ChannelReader<T>. O exemplo acima exige C# 8.0 ou superior, o que o .NET 6 já atende.

O papel do CancellationToken

É importante notar o uso do CancellationToken, que permite cancelar a ação a qualquer momento. Na prática, o SignalR dispara esse token quando o client cancela a inscrição no stream.

Com isso, o servidor para o loop e libera recursos assim que o client se desconecta. Sem o token, o while (true) continuaria rodando à toa.

O que o yield faz

Em adicional, usamos o yield (produtor) para definir que o retorno é um iterador. Desse modo, o client consome nosso conteúdo direto, sem precisar de uma classe para armazenar o estado.

Além disso, os métodos iteradores assíncronos evitam problemas comuns dos Channels. Por exemplo, devolver o ChannelReader tarde demais ou sair do método sem completar o ChannelWriter.

Registrando o SignalR com minimal APIs no Program.cs

Para adicionar suporte ao SignalR na nossa aplicação, precisamos de duas linhas: builder.Services.AddSignalR() e app.MapHub<YourHub>("ENDPOINT").

using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);
// registra os serviços do SignalR
builder.Services.AddSignalR();

var app = builder.Build();
// mapeia o Hub para a rota /chat
app.MapHub<MyHub>("/chat");
app.Run();

O AddSignalR registra os serviços que os Hubs precisam no container de injeção de dependência. Já o MapHub associa a classe do Hub a uma rota da aplicação.

Vale lembrar que o SignalR é um middleware do ASP.NET. Antes do .NET 6, essa mesma configuração exigia ConfigureServices, UseRouting e UseEndpoints dentro da classe Startup.

Código final do SignalR com minimal APIs

O código final completo do nosso servidor com SignalR fica assim.

using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();

var app = builder.Build();
app.MapHub<MyHub>("/chat");
app.Run();

class MyHub : Hub
{
    public async IAsyncEnumerable<DateTime> Streaming(CancellationToken cancellationToken)
    {
        while (true)
        {
            yield return DateTime.UtcNow;
            await Task.Delay(1000, cancellationToken);
        }
    }
}

Repare que o arquivo inteiro tem pouco mais de vinte linhas. Esse é justamente o ganho do SignalR com minimal APIs: a infraestrutura some e sobra apenas o que importa.

Note também que a classe MyHub fica no mesmo arquivo, logo depois das top level statements. Em um projeto real, porém, o ideal é mover o Hub para um arquivo próprio.

Executando o servidor

Agora basta executar o programa usando o comando abaixo.

dotnet watch run

O dotnet watch reinicia ou recarrega a aplicação sempre que você salva um arquivo. Assim, você testa as mudanças no Hub sem parar o servidor manualmente.

Não esqueça de anotar a URL na qual seu servidor está rodando. Vamos precisar dela para nos conectar no client a seguir.

Consumindo o SignalR com minimal APIs no client

Para se conectar ao servidor, vamos criar um Console Application. Dessa forma, fica fácil observar os dados chegando no terminal.

dotnet new console -o SignalClient
cd SignalClient
dotnet add package Microsoft.AspNetCore.SignalR.Client

Novamente, tudo o que precisamos para consumir um serviço SignalR é o pacote Microsoft.AspNetCore.SignalR.Client. Diferente do servidor, aqui o pacote é obrigatório, porque um projeto console não carrega o shared framework do ASP.NET Core.

Conectando ao servidor

Para se conectar ao servidor, precisamos apenas do HubConnectionBuilder.

using Microsoft.AspNetCore.SignalR.Client;

// troque pela URL anotada ao executar o servidor
var uri = "https://localhost:7182/chat";

await using var connection = new HubConnectionBuilder().WithUrl(uri).Build();

O await using garante o descarte da conexão quando o programa termina. Em seguida, podemos iniciar a conexão.

await connection.StartAsync();

Nesse momento, o client negocia com o servidor qual transporte usar. Na maioria dos ambientes locais, o resultado é uma conexão WebSocket.

Recebendo dados em streaming

Como utilizamos o IAsyncEnumerable no servidor, podemos chamar a função StreamAsync<TYPE>("STREAM") para receber os dados.

await foreach (var date in connection.StreamAsync<DateTime>("Streaming"))
{
    Console.WriteLine(date);
}

O parâmetro genérico de StreamAsync<T> define o tipo dos objetos que o método do Hub devolve. Já o texto "Streaming" precisa bater com o nome do método no Hub.

Como resultado final, temos o seguinte código no client.

using Microsoft.AspNetCore.SignalR.Client;

var uri = "https://localhost:7182/chat";

await using var connection = new HubConnectionBuilder().WithUrl(uri).Build();

await connection.StartAsync();

await foreach (var date in connection.StreamAsync<DateTime>("Streaming"))
{
    Console.WriteLine(date);
}

Ao executar o client, uma nova data aparece no terminal a cada segundo. Então, você tem a prova de que o SignalR com minimal APIs está entregando o stream em tempo real.

Outros clients do SignalR

O Console Application é apenas um dos clients possíveis. O SignalR também oferece client JavaScript, distribuído pelo pacote npm @microsoft/signalr, além de clients para Java e outras plataformas.

Dessa maneira, o mesmo servidor de SignalR com minimal APIs atende uma página web, um app mobile e um serviço em background ao mesmo tempo. O contrato continua sendo o nome do método e o tipo dos dados.

No navegador, por exemplo, o client JavaScript também conhece o conceito de stream. Então, a página consegue assinar o método Streaming e atualizar a tela a cada nova data recebida.

Cancelando o stream pelo client

O StreamAsync também aceita um CancellationToken. Quando você chama Cancel no CancellationTokenSource, o client envia uma mensagem de cancelamento ao servidor.

// cancela o stream automaticamente após 10 segundos
using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));

var stream = connection.StreamAsync<DateTime>("Streaming", cancellationTokenSource.Token);

await foreach (var date in stream)
{
    Console.WriteLine(date);
}

Com isso, o token do método Streaming no Hub dispara e o loop do servidor termina. Esse é exatamente o comportamento que preparamos lá atrás com o CancellationToken.

Testando o SignalR com minimal APIs

Com servidor e client prontos, abra dois terminais. No primeiro, rode o SignalServer e, no segundo, rode o SignalClient.

Em seguida, experimente abrir um terceiro terminal com outro client. Cada conexão recebe o próprio stream, porque cada invocação do método Streaming roda de forma independente.

Habilitando logs detalhados

Quando algo não funciona, os logs ajudam muito. O SignalR usa duas categorias de log: Microsoft.AspNetCore.SignalR e Microsoft.AspNetCore.Http.Connections.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug",
      "Microsoft.AspNetCore.Http.Connections": "Debug"
    }
  }
}

A primeira categoria registra a ativação dos Hubs e a invocação dos métodos. Já a segunda mostra detalhes dos transportes, como WebSockets e Long Polling.

Contudo, evite publicar logs de produção em fóruns públicos, porque eles podem conter informações sensíveis. Use o nível Debug apenas durante o diagnóstico.

Boas práticas para SignalR com minimal APIs

O exemplo deste artigo é propositalmente simples. Mesmo assim, alguns cuidados valem para qualquer projeto que use SignalR com minimal APIs.

  • Sempre receba um CancellationToken nos métodos de streaming e repasse para as chamadas assíncronas.
  • Não guarde estado em propriedades do Hub, porque cada chamada usa uma nova instância.
  • Use IHubContext para enviar mensagens a partir de serviços ou endpoints fora do Hub.
  • Considere o protocolo MessagePack quando o volume de mensagens for alto.
  • Ao escalar para vários servidores, avalie o Azure SignalR Service ou um backplane com Redis.

Além disso, organize o código à medida que o projeto cresce. O Program.cs enxuto é ótimo para começar, mas Hubs maiores merecem arquivos e pastas próprias.

Conclusão

Assim como as Minimal APIs, outras implementações como o ASP.NET SignalR também se beneficiam dessas mudanças, tornando o ambiente mais simples. Com apenas AddSignalR e MapHub, o servidor fica pronto.

Em resumo, o SignalR com minimal APIs combina três peças: um Hub, um método que retorna IAsyncEnumerable e um client que consome o stream com StreamAsync. O código completo deste exemplo está no repositório do projeto no GitHub.

FAQ

Preciso instalar o pacote Microsoft.AspNetCore.SignalR no servidor?

Não necessariamente, porque desde o ASP.NET Core 3.0 o servidor do SignalR faz parte do shared framework. No client console, porém, o pacote Microsoft.AspNetCore.SignalR.Client é obrigatório.

Qual a diferença entre IAsyncEnumerable e ChannelReader no SignalR?

Os dois tipos transformam um método do Hub em streaming. O IAsyncEnumerable com yield é mais simples e evita erros comuns dos Channels.

O SignalR sempre usa WebSockets?

Não, ele prefere WebSockets, mas faz fallback para Server-Sent Events e Long Polling quando necessário. A escolha acontece automaticamente na negociação.

Posso consumir o mesmo Hub a partir do navegador?

Sim, basta usar o client JavaScript do pacote npm @microsoft/signalr. O método de streaming funciona da mesma forma.

Como parar o stream quando o client desconecta?

Receba um CancellationToken no método do Hub e repasse para o Task.Delay e demais chamadas. O SignalR dispara o token quando o client cancela ou desconecta.