ASP.NET Core 6 SignalR: Creating Real-Time Charts in Blazor Web Assembly using C3.js

In this article, we will implement the Real-Time Charts in Blazor WebAssembly Application using ASP.NET Core 6 SignalR. Creating Real-Time charts is one of the frequent requirements in Moden-Web Applications. This means that the chart must be generated without page postback. This must be implemented using SignalR. I have already published an article on Chart Creation in Blazor WebAssembly using C3.js. You can read this article from this link.    

What is SignalR?

ASP.NET Core SignalR is an Open-Source library that is used to add simplified real-time functionality to the Moden-Web Applications. The advantage of using Real-Time functionality is that it enables the content generated after server-side after execution to the client-side instantly. The ideal scenarios of using the SignalR are discussed in the following points

  • Dashboard applications where the updates from the server are immediately pushed to the client.
  • Gaming, Social App where high-frequency updates are pushed to the client
  • Video-based Team meeting apps
The SignalR provides API for the communication from the server-to-client using Remote-Procedure-Calls. The main role of the RPC is that it invokes functions on the client from the server-side .NET Code. The advantage of using SignalR is that there are several client SDKs available for the implementation e.g. JavaScript, Java, .Net, etc. ASP.NET Core SignalR has some of the following important features
  • It scales up to handle increasing traffic
  • Sends messages to all connected clients simultaneously
  • Handling automatic connection management 
The SignalR supports various techniques for real-time communication e.g. WebSockets, Long Polling, Server-Sent events. The best transportation method will be automatically chosen by the SignalR within the capabilities of the server and the client. Another important concept of the SignalR is the Hubs. This is a high-level pipeline that allows a client and server to call methods from each other. There are two build-in hub protocols provided by SingalR are A text-based protocol on JSON and a binary protocol based.  

Figure 1, provides a fire idea of the SignalR for application development


Figure 1: The ASP.NET Core 6 SignalR for application

The application is implemented using the .NET 6 SDK and Visual Studio 2022.

Step 1: Open Visual Studio 2022 and create a Blank Solution and name it as ReatTimeChart. In this solution add a new .NET Core Class Library Project targetted to .NET 6. Name this library as SharedModels. In this project add a class file and name it as Market.cs. In this class file, we will add  a new entity class that will contain code as shown in listing 1
public class Market
{
 public string? CompanyName { get; set; }
 public int Volume { get; set; }
}


Listing 1: The Marker class  
 
Step 2: In the solution add a new ASP.NET Core API project targetted to .NET 6. Name this project as ChartServer. In this project add a reference to the SharedModels library project which we have created in Step 1.  

Step 3: In the ChartServer project add a new folder and name it as RHub. In this folder add a new class file and name it as MarketHub.cs. In this class file, we will create a Hub class to manage communication from server to client and back by invoking methods.  The code for the Hub class is provided in listing 2

public class MarketHub : Hub
{
   public async Task AcceptData(List<Market> data) =>
   await Clients.All.SendAsync("CommunicateMarketData", data);
}
Listing 2: Hub class 
As shown in Listing 2, the AcceptData() method will be used to send a list of the Market Data to the active client. The client will receive initial data over the CommunicationMarketData by subscribing to it over a Hub.

Step 4:  In the ChartServer project add a new folder and name it as DataProvider and in this folder add a new class file and name it as TimeWatcher.cs. In this class, file add code as shown in the listing 3

namespace ChartServer.DataProvider
{
    /// <summary>
    /// This call will be used to send the data after each second to the client
    /// </summary>
    public class TimeWatcher
    {
        private Action? Executor;
        private Timer? timer;
        // we need to auto-reset the event before the execution
        private AutoResetEvent? autoResetEvent;


        public DateTime WatcherStarted { get; set; }

        public bool IsWatcherStarted { get; set; }

        /// <summary>
        /// Method for the Timer Watcher
        /// This will be invoked when the Controller receives the request
        /// </summary>
        public void Watcher(Action execute)
        {
            int callBackDelayBeforeInvokeCallback = 1000;
            int timeIntervalBetweenInvokeCallback = 2000;
            Executor = execute;
            autoResetEvent = new AutoResetEvent(false);
            timer = new Timer((object? obj) => {
                Executor();
                if ((DateTime.Now - WatcherStarted).TotalSeconds > 60)
                {
                    IsWatcherStarted = false;
                    timer.Dispose();
                }
            }, autoResetEvent, callBackDelayBeforeInvokeCallback,
            timeIntervalBetweenInvokeCallback);

            WatcherStarted = DateTime.Now;
            IsWatcherStarted = true;
        }
    }
}

Listing 3: The TimeWatcher class            

The reason for adding this class is because we will be creating Real-Time charts and we want the data to be sent to the client from the server to the client after an interval of each second. When the API Controller (which we will be adding in one of the next steps)  is requested the Watcher() method will start executing and after an interval each second the new Marker data will be sent from server-to-client.

In the DataProvider folder add a new class file and name it as MarketDataProvider.cs. In this class file, we will add a class to generate the Market data randomly which we will be sending to the client to generate the chart. The code is shown in listing 4
using SharedModels;

namespace ChartServer.DataProvider
{
    public static class MarketDataProvider
    {
        public static List<Market> GetMarketData()
        {
            var random = new Random();
            var marketData = new List<Market>()
            { 
                new Market() { CompanyName = "MS-IT Services", Volume = random.Next(1,900)},
                new Market() { CompanyName = "TS-IT Providers", Volume = random.Next(1,900)},
                new Market() { CompanyName = "LS-SL Sales", Volume = random.Next(1,900)},
                new Market() { CompanyName = "MS-Electronics", Volume = random.Next(1,900)},
                new Market() { CompanyName = "TS-Electrical", Volume = random.Next(1,900)},
                new Market() { CompanyName = "LS-Foods", Volume = random.Next(1,900)},
                new Market() { CompanyName = "MS-Healthcare", Volume = random.Next(1,900)},
                new Market() { CompanyName = "LS-Pharmas", Volume = random.Next(1,900)},
                new Market() { CompanyName = "TS-Healthcare", Volume = random.Next(1,900)}
            };
            return marketData;
        }
    }
}



Listing 4: The MarketDataProvider class  

Step 5: In the Controllers folder, add a new Empty API Controller and name it as MarketController.cs. In this controller add a code as shown in the listing 5
 
using ChartServer.DataProvider;
using ChartServer.RHub;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

namespace ChartServer.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class MarketController : ControllerBase
    {
        private IHubContext<MarketHub> marketHub;
        private TimeWatcher watcher;

        public MarketController(IHubContext<MarketHub> mktHub, TimeWatcher watch)
        {
            marketHub = mktHub;
            watcher = watch;
        }
        [HttpGet]
        public IActionResult Get()
        {
            if(!watcher.IsWatcherStarted)
            {
                watcher.Watcher(()=>marketHub.Clients.All.SendAsync
                ("SendMarketStatusData"
                ,MarketDataProvider.GetMarketData()));
            }

            return Ok(new { Message = "Request Completed" });
        }
    }
}


Listing 5: The Controller

The Controller class is injected with the IHibContext interface, this is an abstraction for a Hub for sending notifications to clients that are connected to the SignalR server. The TimeWatcher class is also injected to start the timer when the API controller receives the HTTP GET request. The controller uses the SendMarketStatusData method that will be subscribed by the client to receive the randomly generated Market data from the server.

Modify the Program.cs to register the TimerWatcher, CORS policy, and register the  SignalR service in the Dependency Container. Also, add Middlewares for CORS and the SignalR Hub endpoint for communication as shown in listing 6 (Blue Color)
using ChartServer.DataProvider;
using ChartServer.RHub;
using SharedModels;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

// Add CORS Policy

builder.Services.AddCors(option => {
    option.AddPolicy("cors", policy => { 
        policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyHeader();
    });
});
builder.Services.AddSignalR();
// Register the Watcher
builder.Services.AddScoped<TimeWatcher>();

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors("cors");
 app.UseAuthorization();
app.MapControllers();
// Add the SignalR Hub
app.MapHub<MarketHub>("/marketdata");
 app.Run();


Listing 6: The code modification in Program.cs      

That's it, so we have the API with SignalR, now it's time for creating the client application.

Step 6: In the Solution add a new Blazor WebAssembly application targetted to .NET 6.  Name this project as MarketChartClient.  In this project add a reference to the SharedModels project which we have created in Step 1. In this project add  the following package for the SignalR client

Microsoft.AspNetCore.SignalR.Client

Since we will be using C3.js for creating charts, we need to download C3.js from this link. Extract the downloaded zip and after extracting this zip file, copy c3.js. c3.css and 3-5.8.2.min.s in the wwwroot folder of the application.

Step 7: In the wwwroot folder, add a new JavaScript file and name it drawChart.js. In this file, we will add the JavaScript code for generating the LineChart and BarChart as shown in listing 7

function marketLineChart([]) {
    let data = [];
    let labels = [];
    // data points for chart range on Y-Axis
    data = arguments[0];
    // labels on X-Axis
    labels = arguments[1];
    var chart1 = c3.generate({
        bindto: '#market',
        data: {
            columns: [
                data
            ]
        },
        axis: {
            x: {
                type: 'category',
                categories: labels,
                label: {
                    text: 'CompanyName',
                    position: 'outer-center'
                }
            },
            y: {
                label: {
                    text: 'Volume',
                    position: 'outer-center'
                }
            }
        }
    });
}

function marketBarChart([]) {
    let data = [];
    let labels = [];
    data = arguments[0];
    labels = arguments[1];
    var chart = c3.generate({
        bindto: '#market',
        data: {
            columns: [
                data
            ],
            type: 'bar'
        },
        bar: {
            width: {
                ratio: 0.5
            }

        },
        axis: {
            x: {
                type: 'category',
                categories: labels,
                label: {
                    text: 'CompanyName',
                    position: 'outer-center'
                }
            },
            y: {
                label: {
                    text: 'Volume',
                    position: 'outer-center'
                }
            }
        }
    })
}

Listing 7: drawChart.js for generating chart  
 
In listing 7, the c3.generate() method is used to generate the chart in the UI element having id as 'market' (in the code we have used it using the bindto:"#market" property of the generate() method). 

Step 8: In the index.html file of the wwwroot folder, add the references for the c3.js.c3.css and d3-5.8.2.js in the head section and add the script reference of the drawChart.js before the end tag of the body tag of index.html.

Step 9: In the project add a new folder and name it as HttpCaller. In this folder add a new class file and name it as MarketDataCaller.cs.  In this file add the code make a call to API and SignalR hub endpoint as shown in listing 8

namespace MarketChartClient.HttpCaller
{
    public class MarketDataCaller
    {
        private HttpClient httpClient;

        public MarketDataCaller(HttpClient http)
        {
            httpClient = http;
        }

        public async Task GetMarketDataAsync()
        {
            try
            {
                var response = await httpClient.GetAsync("marketdata");

                if (!response.IsSuccessStatusCode)
                    throw new Exception("Something is wrong with the 
                    connection make sure that the server is running.");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw ex;
            }
        }

        public async Task GetMarketEndpoint()
        {
            try
            {
                var response = await httpClient.GetAsync
                ("https://localhost:7193/api/Market");
                if (!response.IsSuccessStatusCode)
                    throw new Exception("Something is wrong with 
                    the connection so get call is not executing.");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw ex;
            }
        }
         
    }
}

Listing 8: The MarketDataCaller class 

As shown in Listing 8, the MarkerDataCaller class is injected with HttpClient class. The GetMarketEndpoint() method makes HTTP GET call to the API endpoint, with this call the timer on the server will start and SignalR Hub will start sending data. The GetMarketDataAsync() method will call the marketdata Hub endpoint and will start receiving random Market data from server to client.  

Modify the Program.cs and register the MarketDataCaller class in the dependency container of the application as shown in listing 9
builder.Services.AddScoped<MarketDataCaller>();

Listing 9: The Dependency registration of the MarkerDataCaller class    

Step 10: In the pages folder, add a new razor component and name it as ChartComponent.razor. This component will be injected with the MarketDataCaller class to call the API and connect to SingalR Hub. This component is also be injected using IJSRuntime to access the drawChart.js to send the received Market Data to generate the Line and Bar chart. In this component add the code as  shown in listing 10

@page "/chartui"
@using  Microsoft.AspNetCore.SignalR.Client;
@using SharedModels
@using System.Text.Json
@inject IJSRuntime js
@inject MarketChartClient.HttpCaller.MarketDataCaller service;
<div>
    <div class="container">
    <table class="table table-bordered table-striped">
        <tbody>
            <tr>
                <td>
                    <button class="btn btn-success"
                    @onclick="@generateLineChartTask">Line Chart</button>
                </td>
                 <td>
                    <button class="btn btn-danger" 
                    @onclick="@generateBarChartTask">Bar Chart</button>
                </td>
            </tr>
        </tbody>
    </table>
    <div id="market"></div>
    <table class="table table-bordered table-striped">
        <thead>
            <tr>
                <th>Company Name</th>
                <th>Volume</th>
            </tr>
        </thead>
        <tbody>
           @foreach (var item in MarketData)
           {
               <tr>
                   <td>@item.CompanyName</td>
                   <td>@item.Volume</td>
               </tr>
           }
        </tbody>
    </table>
    <hr/>
    <div class="container">
        @ConnectionStatusMessage
    </div>
</div>
</div>
@code {
    private HubConnection? hubConn;
    private string? ConnectionStatusMessage;
    public List<Market> MarketData = new List<Market>(); 
    public List<Market> MarketReceivedData = new List<Market>(); 


    private  List<string> xSource;
    private  List<int> ySource;
    private List<object> source; 

    protected override async Task OnInitializedAsync()
    {

        xSource = new List<string>();
        ySource = new List<int>();
        source = new List<object>();  

        await service.GetMarketEndpoint();
        await Start();
         
    }


    private async Task Start()
    {
        hubConn = new HubConnectionBuilder().WithUrl
        ("https://localhost:7193/marketdata").Build();
        await hubConn.StartAsync();
        if(hubConn.State == HubConnectionState.Connected )
            ConnectionStatusMessage = "Connection is established Successfully...";
        else
            ConnectionStatusMessage = "Connection is not established...";
    }

    private void MarketDataListener(string chartType)
    {
        hubConn.On<List<Market>>("SendMarketStatusData", 
        async (data) =>
        {
            MarketData = new List<Market>(); 
            foreach (var item in data)
            {
                Console.WriteLine($"Company Name: {item.CompanyName},
                Volumn: {item.Volume}");
                xSource.Add(item.CompanyName);
                ySource.Add(item.Volume);
            }


            source.Add(ySource);
            source.Add(xSource);


            MarketData = data;

            StateHasChanged();
            await js.InvokeAsync<object>(chartType, source.ToArray());
            xSource.Clear();
            ySource.Clear();
        });
    }

    private void ReceivedMarketDataListener()
    {
        hubConn.On<List<Market>>("CommunicateMarketData",
        (data) =>
        {
            MarketReceivedData = data;
            StateHasChanged();
        });
    }

 

    public async Task Dispose()
    {
        await hubConn.DisposeAsync();
    }


    async Task  generateLineChartTask()
    {
        MarketDataListener("marketLineChart");
         ReceivedMarketDataListener();
        await service.GetMarketDataAsync();
    }
     async Task   generateBarChartTask()
    {
      MarketDataListener("marketBarChart"); 
       ReceivedMarketDataListener();
        await service.GetMarketDataAsync();
    }
}

Listing 10: The component code
The code in the Listing 10 is the heart of the application. The Start() method establishes a connection with the SignalR based on the marketdata endpoint exposed by the server using the HubConnectionBuilder class. This class is a builder for configuring HubConnection instances so that data communication can be started from SignalR Hub. The MarketDataListener() and ReceivedMarketDataListener() methods are responsible to receive the random data from the SignalR Hub. The OnInitializedAsync() method is executed when the component is loaded and this method calls the GetMarketEndpoint() method from the MarketDataCaller class. This method will make an HTTP request to API and the timer will start that will start sending the randomly generated Market data to the client. The generateLineChartTask() and generateBarChartTask() methods will call MarketDataListener(), ReceivedMarketDataListener() and GetMarketDataAsync()  methods to perform data communication to receive the Market Data.  The generateLineChartTask() and generateBarChartTask() methods are bound with the buttons to generate Line and Bar charts.    

Step 11: Modify the NavMenu.razor file in the Shared folder to define the Navigation link for the ChartComponent.razor as shown in listing 11
 <div class="nav-item px-3">
            <NavLink class="nav-link" href="chartui">
                <span class="oi oi-list-rich" 
                aria-hidden="true"></span> Chart
            </NavLink>
        </div>


Listing 11: The Navigation link
  
Run the ChartServer application and then run the MarketClient application. The MarkerClient application will be loaded in the browser, this result will show the Chart link. Click on the Chart link and this will show the result as shown in figure 2 



Figure 2: The MarketClient app 

Click on the Line Chart and then Bar Chart, these charts will be generated based on the randomly generated Market data received from the ChartServer as shown in the following video




The code for this article can be downloaded from this link.

Conclusion: The ASP.NET Core SignalR is the best technology for building Modern Web Applications with Real-Time Data communication.
  

Comments

Post a Comment

Popular posts from this blog

ASP.NET Core 6: Adding Custom Middleware and Logging the Error Message in Database

gRPC Services using .NET 5: Using Entity Framework Core with gRPC for Performing Database Operations

Passing Data Across Blazor Components