SignalR é uma biblioteca gratuita de código aberto para ASP.NET Core que permite que o servidor envie mensagens assíncronas em tempo real para clientes conectados. É uma camada de abstração sobre WebSockets, tornando-o mais fácil de usar e fornecendo fallback para outras formas de comunicação quando necessário (eventos enviados pelo servidor e long polling).
Neste post, mostrarei como construir um aplicativo Blazor WebAssembly que exibe gráficos em tempo real de um servidor SignalR.
Aplicativo em execução
A estrutura do projeto
O projeto terá 4 projetos, criados usando 2 modelos de projeto:
Os clientes SignalR se conectam a Hubs, que são componentes que definem métodos que podem ser chamados pelos clientes para enviar mensagens ou para se inscrever em mensagens do servidor.
Nesta demonstração, criei um Hub com dois métodos que retornam, cada um, um fluxo de dados simulando uma mudança no preço da moeda. Quando o cliente chama os métodos, ele adicionará o ConnectionId do cliente a uma lista de ouvintes e retornará o fluxo de dados para o cliente. Depois disso, o serviço RealTimeDataStreamWriter gravará cada mudança no preço da moeda nos fluxos dos ouvintes.
O OnDisconnectedAsync é chamado quando um cliente se desconecta e remove o cliente da lista de ouvintes.
⚠️ Esta é uma implementação ingênua que não é horizontalmente escalável. É apenas para fins de demonstração. Além disso, para escalar o SignalR horizontalmente, um plano de dados Redis deve ser configurado.
ApexCharts é uma biblioteca JavaScript gratuita de código aberto para gerar gráficos interativos e responsivos. Ele tem uma ampla gama de tipos de gráficos. É a melhor biblioteca gratuita que encontrei para trabalhar com gráficos em tempo real, com animações fluidas.
ApexCharts para Blazor é uma biblioteca wrapper para trabalhar com ApexCharts em Blazor. Ele fornece um conjunto de componentes Blazor que tornam mais fácil usar os gráficos em aplicativos Blazor.
Exemplos de ApexCharts para Blazor
O Frontend - Blazor WebAssembly
No projeto Blazor WebAssembly, precisamos instalar os pacotes NuGet Blazor-ApexCharts e Microsoft.AspNetCore.SignalR.Client:
@page "/"
@using BlazorWasmSignalR.Wasm.Shared
@using System.Security.Cryptography;
<PageTitle>Gráficos em tempo real no Blazor WebAssembly</PageTitle><h1>Gráficos em tempo real no Blazor WebAssembly</h1><divclass="chart-container"><divclass="radial-chart"><ApexChartTItem="DataItem"Title="Transactions"Options="@_radialChartOptions"@ref="_radialChart"><ApexPointSeriesTItem="DataItem"Items="_radialData"SeriesType="SeriesType.RadialBar"Name="Variation"XValue="@(e => "Variation")"YAggregate="@(e => e.Average(e => e.Value))"/></ApexChart></div><divclass="line-chart"><ApexChartTItem="DataItem"Title="Currency Exchange Rates in USD"Options="@_lineChartOptions"@ref="_lineChart"><ApexPointSeriesTItem="DataItem"Items="_yenSeries"Name="Yen"SeriesType="SeriesType.Line"XValue="@(e => e.Minute)"YAggregate="@(e => e.Sum(e => e.Value))"/><ApexPointSeriesTItem="DataItem"Items="_euroSeries"Name="Euro"SeriesType="SeriesType.Line"XValue="@(e => e.Minute)"YAggregate="@(e => e.Sum(e => e.Value))"/></ApexChart></div></div>
Conectando ao fluxo SignalR
Para conectar a um fluxo SignalR, criamos uma conexão com o Hub usando a classe HubConnectionBuilder e abrimos a conexão usando o método StartAsync da classe HubConnection.
Em seguida, nos inscrevemos no fluxo usando o método StreamAsChannelAsync, passando o nome do fluxo.
ℹ️ Observe que uma conexão pode ser usada para se inscrever em muitos fluxos.
Atualizando os valores do gráfico em tempo real
Para ler os dados do fluxo, uso o método WaitToReadAsync da classe ChannelReader para esperar por novas mensagens e, em seguida, percorro-as com o método TryRead.
Em seguida, adiciono os valores à série e chamo o método UpdateSeriesAsync do gráfico para forçar uma nova renderização.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
privateasyncTaskReadCurrencyStreamAsync(ChannelReader<CurrencyStreamItem>channelCurrencyStreamItem){// Aguarde assincronamente que os dados se tornem disponíveiswhile(awaitchannelCurrencyStreamItem.WaitToReadAsync()){// Leia todos os dados atualmente disponíveis de forma síncrona, antes de esperar por mais dadoswhile(channelCurrencyStreamItem.TryRead(outvarcurrencyStreamItem)){_yenSeries.Add(new(currencyStreamItem.Minute,currencyStreamItem.YenValue));_euroSeries.Add(new(currencyStreamItem.Minute,currencyStreamItem.EuroValue));await_lineChart.UpdateSeriesAsync();}}}
⚠️ Como quero que as atualizações sejam assíncronas, não await nos métodos ReadCurrencyStreamAsync e ReadVariationStreamAsync.
privatereadonlyIList<DataItem>_yenSeries=newList<DataItem>();privatereadonlyIList<DataItem>_euroSeries=newList<DataItem>();privateApexChart<DataItem>_lineChart=default!;privateApexChart<DataItem>_radialChart=default!;privateApexChart<DataItem>_lineChart=default!;protectedoverrideasyncTaskOnInitializedAsync(){_radialData=newDataItem[1]{new(DateTime.Now.ToString("mm:ss"),0)};//Inicialize os dados para o gráfico radialvarconnection=newHubConnectionBuilder().WithUrl(_configuration["RealtimeDataUrl"]!).Build();awaitconnection.StartAsync();varchannelCurrencyStreamItem=awaitconnection.StreamAsChannelAsync<CurrencyStreamItem>("CurrencyValues");varchannelVariation=awaitconnection.StreamAsChannelAsync<DataItem>("Variation");_=ReadCurrencyStreamAsync(channelCurrencyStreamItem);_=ReadVariationStreamAsync(channelVariation);}privateasyncTaskReadCurrencyStreamAsync(ChannelReader<CurrencyStreamItem>channelCurrencyStreamItem){// Aguarde assincronamente que os dados se tornem disponíveiswhile(awaitchannelCurrencyStreamItem.WaitToReadAsync()){// Leia todos os dados atualmente disponíveis de forma síncrona, antes de esperar por mais dadoswhile(channelCurrencyStreamItem.TryRead(outvarcurrencyStreamItem)){_yenSeries.Add(new(currencyStreamItem.Minute,currencyStreamItem.YenValue));_euroSeries.Add(new(currencyStreamItem.Minute,currencyStreamItem.EuroValue));await_lineChart.UpdateSeriesAsync();}}}privateasyncTaskReadVariationStreamAsync(ChannelReader<DataItem>channelVariation){// Aguarde assincronamente que os dados se tornem disponíveiswhile(awaitchannelVariation.WaitToReadAsync()){// Leia todos os dados atualmente disponíveis de forma síncrona, antes de esperar por mais dadoswhile(channelVariation.TryRead(outvarvariation)){_radialData[0]=variation;await_radialChart.UpdateSeriesAsync();}}}
Inspecionando as mensagens
Entrando nas ferramentas de desenvolvedor do navegador, podemos ver as mensagens na conexão WebSocket: