SignalR is a free open-source library for ASP.NET Core that allows the server to push real-time asynchronous messages to connected clients. It is an abstraction layer on top of WebSockets, making it easier to use and providing fallback to other forms of communication when necessary (server-sent events and long polling).
In this post, I’ll show how to build a Blazor WebAssembly app that displays real-time charts from a SignalR server.
Application running
The project structure
The project will have 4 projects, created using 2 project templates:
ASP.NET Core Project (for the SignalR server)
BlazorWasmSignalR.SignalRServer
Blazor WebAssembly App (ASP.NET Core Hosted)
BlazorWasmSignalR.Wasm.Client (Blazor WASM)
BlazorWasmSignalR.Wasm.Server (ASP.NET Core Host)
BlazorWasmSignalR.Wasm.Shared (Common components)
Project structure
The Backend - SignalR Server
SignalR is part of ASP.NET Core. To use it, we just need to configure it in our Startup.cs or Program.cs (if using top-level statements):
SignalR clients connect to Hubs, which are components that define methods that can be called by the clients to send messages or to subscribe to messages from the server.
In this demo, I’ve created a Hub with two methods that return, each, a stream of data simulating a currency price change. When the client calls the methods, it will add the client’s ConnectionId to a list of listeners and return the stream of data to the client. After that, the RealTimeDataStreamWriter service will write every currency price change to the listeners streams.
The OnDisconnectedAsync is called when a client disconnects and removes the client from the listeners list.
⚠️ This is a naive implementation that is not horizontally scalable. It is for demo purposes only. Also, for scaling SignalR horizontally, a Redis dataplane must be configured.
ApexCharts is a free open-source JavaScript library to generate interactive and responsive charts. It has a wide range of chart types. It is the best free library that I found for working with real-time charts, with fluent animations.
ApexCharts for Blazor is a wrapper library for working with ApexCharts in Blazor. It provides a set of Blazor components that makes it easier to use the charts within Blazor applications.
ApexCharts for Blazor examples
The Frontend - Blazor WebAssembly
In the Blazor WebAssembly project, we need to install the Blazor-ApexCharts and Microsoft.AspNetCore.SignalR.Client nuget packages:
To connect to a SignalR stream, we create a connection to the Hub using the HubConnectionBuilder class and open the connection using the StartAsync method of the HubConnection class.
Then, we subscribe to the stream using the StreamAsChannelAsync method, passing the stream name.
ℹ️ Note that one connection can be used to subscribe to many streams.
Updating the chart values in real-time
To read the data from the stream, I use the method WaitToReadAsync of the ChannelReader class to wait for new messages and then loop through them with the TryRead method.
Then, I add the values to the series and call the UpdateSeriesAsync method of the chart to force a re-render.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
privateasyncTaskReadCurrencyStreamAsync(ChannelReader<CurrencyStreamItem>channelCurrencyStreamItem){// Wait asynchronously for data to become availablewhile(awaitchannelCurrencyStreamItem.WaitToReadAsync()){// Read all currently available data synchronously, before waiting for more datawhile(channelCurrencyStreamItem.TryRead(outvarcurrencyStreamItem)){_yenSeries.Add(new(currencyStreamItem.Minute,currencyStreamItem.YenValue));_euroSeries.Add(new(currencyStreamItem.Minute,currencyStreamItem.EuroValue));await_lineChart.UpdateSeriesAsync();}}}
⚠️ Because I want the updates to be asynchronous, I do not await on the ReadCurrencyStreamAsync and ReadVariationStreamAsync methods.
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)};//Initialize the data for the radial chartvarconnection=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){// Wait asynchronously for data to become availablewhile(awaitchannelCurrencyStreamItem.WaitToReadAsync()){// Read all currently available data synchronously, before waiting for more datawhile(channelCurrencyStreamItem.TryRead(outvarcurrencyStreamItem)){_yenSeries.Add(new(currencyStreamItem.Minute,currencyStreamItem.YenValue));_euroSeries.Add(new(currencyStreamItem.Minute,currencyStreamItem.EuroValue));await_lineChart.UpdateSeriesAsync();}}}privateasyncTaskReadVariationStreamAsync(ChannelReader<DataItem>channelVariation){// Wait asynchronously for data to become availablewhile(awaitchannelVariation.WaitToReadAsync()){// Read all currently available data synchronously, before waiting for more datawhile(channelVariation.TryRead(outvarvariation)){_radialData[0]=variation;await_radialChart.UpdateSeriesAsync();}}}
Inspecting the messages
Going into the browser’s developer tools, we can see the messages in the WebSocket connection: