SignalR adalah suatu sekumpulan pustaka library yang mendukung komunikasi secara real time dua arah antara server dan klien. SignalR dibangun di atas protokol WebSocket , meskipun demikian SignalR mampu berjalan apabila protokol transport websocket tidak didukung. SignalR membangun koneksi persisten antara server dan klien. SignalR menghadirkan dukungan komunikasi secara real time dua arah, memungkinkan server untuk melakukan push konten ke aplikasi klien tanpa perlu menunggu perintah dari klien, serta memungkinkan server dan klien untuk melakukan Remote Procedure Call (RPC) panggilan eksekusi jarak jauh sehingga memungkinkan komunikasi dua arah selama koneksi antara server dan klien masih berlangsung. SignalR juga memungkinkan Server untuk membroadcast pesan ke seluruh client yang terhubung ke server , atau hanya mengirimkan push data ke salah satu client saja.

SignalR bukanlah websocket dan SignalR bukanlah merek dagang dari protokol websocket. SignalR merupakan library yang dapat menentukan sendiri protokol transport apa yang terbaik / yang didukung oleh klien+server yang akan digunakan untuk komunikasi ketika negosiasi awal terjadi. Berikut adalah protokol/ transport yang didukung oleh SignalR:
- Websocket merupakan transport default / utama yang didukung dan diutamakan oleh SignalR karena sifatnya yang full duplex, low latency, dan sangat efisien. Kebanyakan browser modern sudah mendukung websocket. Penggunaan websocket menjadi lebih efisien dan cepat karena menghilangkan kebutuhan HTTP polling / permintaan berulang ke server yang memerlukan pembukaan penutupan koneksi berkali kali sehingga menyebabkan overhead.
- Server Sent Event (SSE) yang merupakan model transport yang didukung oleh HTML5 yang memungkinkan komunikasi unidirectional dari server ke client. SSE ini kelebihannya didukung html5 dan protokol HTTP standar tanpa perlu library tambahan pihak ketiga, dan dapat diakses dari javascript melalui object EventSource. SSE mendukung model asinkronus dan berbasis event, yang bekerja dengan cara membuka koneksi satu koneksi yang terus terbuka dan mengirimkan data dalam bentuk text/event-stream dari server ke client.
- Long Polling merupakan model transport yang mensimulasikan komunikasi real time dengan cara membuka koneksi HTTP dan menutup koneksi berkali kali sehingga seolah oleh terjadi koneksi real time dua arah antara server dan klien. Klien mengecek data di server berkali kali setiap interval waktu tertentu dan ketika di server sudah tersedia data baru, maka data tersebut diproses oleh klien. Model transport ini hanya akan digunakan ketika dua model transport di atas tidak tersedia.
SignalR menyederhanakan, mengabstraksi, dan menyembunyikan lapisan kompatibilitas transport , menghilangkan kebutuhan mengelola transport dan buka tutup koneksi ke server, sehingga pengembang bisa lebih fokus pada fitur aplikasi dan bisa lebih fokus pada data yang dipertukarkan dan pemrosesan data yang diterima. Dengan demikian SignalR menjadi solusi yang lebih handal dan efisien dalam mengembangkan aplikasi.
Kelebihan SignalR
- Pemilihan model transport otomatis, memilih transport terbaik dan jika tidak ada / tidak didukung maka kembali menggunakan model transport lama.
- Manajemen koneksi otomatis termasuk penanganan buka tutup koneksi.
- Kompatibilitas yang tinggi karena mendukung berbagai platform web browser dan aplikasi client baik desktop maupun mobile
- API Library yang mudah digunakan, memungkinkan pemanggilan fungsi javascript dari server maupun pemanggilan fungsi kode di sisi server dari aplikasi klien.
- Skalabilitas tinggi karena dapat diekspansi dan diperbesar skalanya menggunakan Azure Service Bus/Azure App Service, SQL Server, maupun Redish. Dengan menggunakan backplane dari pihak ketiga (Azure Service Bus/Azure App Service, SQL Server, Redish) memungkinkan server signalR diperbesar menjadi banyak server di dalam server farm.

MEMBANGUN APLIKASI BERBASIS SIGNALR
Dengan menggunakan SignalR kita dapat membangun aplikasi interaktif yang mendukung komunikasi realtime dua arah seperti chat interaktif, dashboard, papan skor hasil pertandingan olah raga, dan lain sebagainya.
Di sini kita akan membangun aplikasi dashboard yang menampilkan data realtime dari performa cpu PC laptop. Dashboard yang kita bangun akan menampilkan grafik realtime performa CPU dan Memory layaknya task manager PC desktop.

Sebelum membuat Hub dan halaman dashboard dengan signalR, kita terlebih dahulu membuat class yang berkaitan dengan CPU Metric Reader. Class ini nantinya bertugas menyediakan data CPU + Ram Utilization yang akan ditampilkan ke dashboard melalui signalR hub. Dikarenakan OS web server yang menjalankan ASP.NET Core tidak hanya windows (bisa jadi linux/unix), maka kita perlu membuat class factory yang otomatis membuat object dari class CPU Metric Reader tergantung Platform OS tanpa perlu diketahui oleh class caller, dan jika implementasi tidak tersedia maka akan dihasilkan dummy CPU Metric Reader. Gambar 4 berikut ini adalah gambar class diagram dari CPU Metric Reader.

Selanjutnnya kita mulai membuat aplikasinya langsung dengan menggunakan visual studio 2022. Pilih template project ASP.NET Core Web App (Model-View-Control) , lalu buat project dengan nama WebApplicationSignalR. Kemudian pilih target framework ke .NET 9.0.



Pada bagian dependency , tambahkan package Nuget berikut ini:
- Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
- Microsoft.Extensions.Diagnostics.ResourceMonitoring

Selanjutnya tambahkan client script library yang diperlukan untuk aplikasi ini, yaitu library signalR dan library chart untuk menampilkan grafik. Caranya, pada folder project wwwroot, klik kanan, kemudian pilih Add > Client Side Library.
- Tambahkan dari unpkg library @microsoft/signalr dan taruh di folder wwwroot/lib/signalr/

- Tambahkan dari cdnjs library Chart.js dan taruh di folder wwwroot/lib/Chart.js/

Apabila nuget package dan client script library sudah ditambahkan, kita terlebih dahulu membuat kode untuk CPU Metric Reader yang sesuai dengan class diagram di atas (Gambar 3).
Pada folder Models, buat new file CPUMetricVM.cs dan tambahkan kode berikut ini
public class CPUMetricVM
{
public int LogicalProcessorCount { get; set; }
public double? CPUPercentUtility { get; set; }
public double? CPUSpeedMHz { get; set; }
public double? MemoryAvailableMB { get; set; }
public double? MemoryUsageMB { get; set; }
public double? MemoryUsagePercent { get; set; }
public double? MemoryTotalMB { get; set; }
}
Tambahkan kode interface ICPUMetricReader
public interface ICPUMetricReader
{
CPUMetricVM ReadCurrentUtilization();
}
Kita Buat class implementasi untuk OS Windows
public class WindowsBasedCPUMetricReader : ICPUMetricReader
{
private IServiceProvider svcProvider;
public WindowsBasedCPUMetricReader(IServiceProvider serviceProvider)
{
svcProvider = serviceProvider;
}
public CPUMetricVM ReadCurrentUtilization()
{
var monitor = svcProvider.GetService<IResourceMonitor>();
var utilization = monitor.GetUtilization(new TimeSpan(0, 0, 0, 0,500));
var metric = new CPUMetricVM();
var perfCounter = new PerformanceCounter("Processor Information", "% Processor Utility", "_Total", true);
perfCounter.NextValue();
var cpu = perfCounter.NextValue();
metric.CPUPercentUtility = cpu ;
metric.LogicalProcessorCount = Convert.ToInt32( utilization.SystemResources.MaximumCpuUnits );
var perfCounter2 = new PerformanceCounter("Processor Information", "Actual Frequency", "_Total", true);
perfCounter2.NextValue();
var mhz = perfCounter2.NextValue();
metric.CPUSpeedMHz = mhz;
PerformanceCounter memCounter = new PerformanceCounter("Memory", "Available MBytes");
float availableMemoryMB = memCounter.NextValue();
metric.MemoryAvailableMB = availableMemoryMB;
metric.MemoryTotalMB = utilization.SystemResources.MaximumMemoryInBytes / 1024.0 / 1024.0;
metric.MemoryUsageMB = metric.MemoryTotalMB - metric.MemoryAvailableMB;
metric.MemoryUsagePercent = metric.MemoryUsageMB * 100.0 / metric.MemoryTotalMB;
return metric;
}
}
Kemudian kita buat implementasi class reader untuk OS Linux / Ubuntu
public class LinuxBasedCPUMetricReader : ICPUMetricReader
{
private IServiceProvider svcProvider;
public LinuxBasedCPUMetricReader(IServiceProvider serviceProvider)
{
svcProvider = serviceProvider;
}
public CPUMetricVM ReadCurrentUtilization()
{
var output = "";
var info = new ProcessStartInfo("free -m");
info.FileName = "/bin/bash";
info.Arguments = "-c \"free --mega\"";
info.RedirectStandardOutput = true;
using (var process = Process.Start(info))
{
output = process.StandardOutput.ReadToEnd();
}
var lines = output.Split("\n");
var memory = lines[1].Split(" ", StringSplitOptions.RemoveEmptyEntries);
var info2 = new ProcessStartInfo("cat /proc/cpuinfo | grep processor");
info2.FileName = "/bin/bash";
info2.Arguments = "-c \"cat /proc/cpuinfo | grep processor\"";
info2.RedirectStandardOutput = true;
using (var process = Process.Start(info2))
{
output = process.StandardOutput.ReadToEnd();
}
var lines2 = output.Split("\n", StringSplitOptions.RemoveEmptyEntries);
var cpuinfo = lines2[lines2.Length - 1].Split(":", StringSplitOptions.RemoveEmptyEntries);
var info3 = new ProcessStartInfo("cat /proc/cpuinfo | grep MHz");
info3.FileName = "/bin/bash";
info3.Arguments = "-c \"cat /proc/cpuinfo | grep MHz\"";
info3.RedirectStandardOutput = true;
using (var process = Process.Start(info3))
{
output = process.StandardOutput.ReadToEnd();
Console.WriteLine(output);
}
var lines3 = output.Split("\n", StringSplitOptions.RemoveEmptyEntries);
var cpumhz = lines3[lines3.Length - 1].Split(":", StringSplitOptions.RemoveEmptyEntries);
var info4 = new ProcessStartInfo("cat /proc/stat |grep cpu |tail -1|awk '{print ($5*100)/($2+$3+$4+$5+$6+$7+$8+$9+$10)}'|awk '{print 100-$1}'");
info4.FileName = "/bin/bash";
info4.Arguments = "-c \"cat /proc/stat |grep cpu |tail -1|awk '{print ($5*100)/($2+$3+$4+$5+$6+$7+$8+$9+$10)}'|awk '{print 100-$1}'\"";
info4.RedirectStandardOutput = true;
using (var process = Process.Start(info4))
{
output = process.StandardOutput.ReadToEnd();
Console.WriteLine(output);
}
var lines4 = output.Split("\n", StringSplitOptions.RemoveEmptyEntries);
var metric = new CPUMetricVM();
metric.MemoryTotalMB = double.Parse(memory[1]);
metric.MemoryUsageMB = double.Parse(memory[2]);
metric.MemoryUsagePercent = metric.MemoryUsageMB*100.0/ metric.MemoryTotalMB;
metric.MemoryAvailableMB = double.Parse(memory[3]);
metric.LogicalProcessorCount = int.Parse(cpuinfo[1]) + 1;
metric.CPUSpeedMHz = double.Parse(cpumhz[1]);
metric.CPUPercentUtility = double.Parse(lines4[0]);
return metric;
}
}
Kemudian, kita buat class factory untuk meng-create CPU Metric Reader dan menentukan implementasi class secara otomatis
public class CPUMetricReaderFactory
{
private IServiceProvider svcProvider;
public CPUMetricReaderFactory(IServiceProvider serviceProvider)
{
svcProvider = serviceProvider;
}
public ICPUMetricReader GetCPUMetricReader()
{
//// uncomment if just want to get random value
//return new DummyCPUMetricReader(svcProvider);
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
return new WindowsBasedCPUMetricReader(svcProvider);
}
else if (Environment.OSVersion.Platform == PlatformID.Unix)
{
return new LinuxBasedCPUMetricReader(svcProvider);
}
else
{
//return new DummyCPUMetricReader(svcProvider);
throw new NotImplementedException("Unknwon OS implementation");
}
}
}
Pekerjaan selanjutnya adalah kita membuat SignalR Hub terlebih dahulu. SignalR Hub inilah yang nantinya dipanggil oleh signalR client ketika client merequest permintaan terbaru, dan server meresponse dengan memanggil event / fungsi js ReceiveData yang ada di lokal client
Untuk membuat SignalR hub adalah dengan menambahkan C# class kosong seperti biasa, namun class yang dibuat turunan dari class Hub. Tambahkan class Hub tersebut di dalam folder Controller. Berikut adalah kode dari class PerformanceHub
public class PerformanceHub: Hub
{
private IServiceProvider svcProvider;
private CPUMetricReaderFactory metricReaderFactory;
public PerformanceHub(IServiceProvider serviceProvider, CPUMetricReaderFactory cpuMetricReaderFactory)
{
svcProvider = serviceProvider;
metricReaderFactory = cpuMetricReaderFactory;
}
public async Task LoadPerformanceData()
{
var reader = metricReaderFactory.GetCPUMetricReader();
var performance = reader.ReadCurrentUtilization();
await Clients.Caller.SendAsync("ReceiveData", performance);
}
}
Buka file _Layout.cshtml (di dalam folder /Views/Shared/) dan tambahkan script script yang diperlukan untuk signalR dan charting. Tambahkan pada header HTML.
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/lib/Chart.js/chart.umd.min.js"></script>
Selanjutnya kita membuat html dan javascript untuk mengambil data dan menggambar grafik. Buka file index.cshtml yang ada di dalam folder /Views/Home. Paste kode html berikut untuk tampilan dasar grafik.
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://bayuprn.com"> building Web apps with ASP.NET Core</a>.</p>
</div>
<div class="row col-md-12">
</div>
<div class="row col-md-12">
</div>
<div id="graphArea" class="row col-md-12">
<div class="col-5 row padding5">
<div class="row col-md-12 padding5">
<h1><b>CPU</b></h1>
<b>% Utilization</b>
</div>
<div class="row col-md-12 padding5" style="min-height:400px; background-color: black;">
<canvas id="graph-cpu"></canvas>
</div>
<div class="row col-md-12 padding5">
<div class="col-md-4 padding5">
<b>Utilzation</b><br/>
<span id="labelCPUUtilization" style="font-size:30px">0 %</span>
</div>
<div class="col-md-4 padding5">
<b>Speed</b><br />
<span id="labelCPUSpeed" style="font-size:30px">0 MHz</span>
</div>
<div class="col-md-4 padding5">
<b>Logical Proc Count</b><br />
<span id="labelLogicalProc" style="font-size:30px">1</span>
</div>
</div>
</div>
<div class="col-2 padding5">
</div>
<div class="col-5 padding5">
<div class="row col-md-12 padding5">
<h1><b>Memory</b></h1>
<b>Memory Usage</b>
</div>
<div class="row col-md-12 padding5" style="min-height:400px; background-color: black;">
<canvas id="graph-mem"></canvas>
</div>
<div class="row col-md-12 padding5">
<div class="col-md-6 padding5">
<b>In Use</b><br />
<span id="labelMemInUse" style="font-size:30px">0 GB (0%)</span>
</div>
<div class="col-md-3 padding5">
<b>Available</b><br />
<span id="labelMemAvailable" style="font-size:30px">0 GB</span>
</div>
<div class="col-md-3 padding5">
<b>Total Memory</b><br />
<span id="labelMemTotal" style="font-size:30px">0 GB</span>
</div>
</div>
</div>
</div>
<div class="row col-md-12">
</div>
Tambahkan kode javascript berikut untuk menginisiasi signalR di client/javascript. Di sini, kita menambahkan handle terhadap event onReceiveData, dimana kita akan memanggil fungsi renderPerformance yang merefresh chart grafik.
<script type="text/javascript">
var rconnection = (new signalR.HubConnectionBuilder()).withUrl("/performance").build();
rconnection.on("ReceiveData", function(metric){
console.log(metric );
renderPerformance(metric);
});
function loadPerformanceData(){
rconnection.invoke("LoadPerformanceData").catch(function(e){
return console.error(err.toString());
});
}
</script>
Tambahkan kode javascript berikut untuk memulai menjalankan signalR ketika onload halaman berhasil. Aplikasi akan menjalankan signalR, menginisiasi chart, menjalankan timer dan memanggil fungsi loadPerformanceData.
<script type="text/javascript">
var timerRefresh;
$(function(){
rconnection.start()
.then(function(){
initCPU();
initMemory();
timerRefresh = setInterval(function(){
loadPerformanceData();
}, 1500);
})
.catch(function(e){
return console.error(err.toString());
});
});
</script>
Lantas bagaimana untuk menampilkan grafiknya? Untuk menampilkan grafik, kita menggunakan Chart.js yang kompatibel dengan JQuery. Tambahkan function javascript berikut ini untuk menampilkan chart LineChart
<script type="text/javascript">
var performanceData = new Array(60);
function renderPerformance(data){
if(window.performanceData ==null || typeof(window.performanceData)=='undefined'){
window.performanceData = new Array(60);
}
window.performanceData.push(data );
if(window.performanceData != null && window.performanceData.length > 60){
window.performanceData.splice(0,window.performanceData.length-60);
}
drawCPU();
drawMemory();
}
var linechart = null;
function initCPU(){
var labels = new Array();
for(var i=0;i<60; i++){
labels.push(i);
}
const data = {
labels: labels,
datasets: [
{
label: '% CPU Utility',
data: new Array(60),
borderColor: 'green',
backgroundColor: 'lightgreen',
fill: true
}
]
};
const config = {
type: 'line',
data: data,
options: {
responsive: true,
plugins: {
title: {
display: true,
text: (ctx) => '% CPU Utilization'
},
tooltip: {
mode: 'index'
},
legend:{
display: false
},
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
scales: {
x: {
beginAtZero: true,
ticks:{
display: false,
},
title: {
display: false,
text: ''
}
},
y: {
min: 0,
max: 100,
stacked: false,
beginAtZero: true,
ticks:{
display: false,
},
title: {
display: false,
text: '%'
}
}
}
}
};
window.linechart = new Chart(
$('#graph-cpu')[0],
config
);
window.linechart.options.animation = false; // disables all animations
window.linechart.options.animations.colors = false; // disables animation defined by the collection of 'colors' properties
window.linechart.options.animations.x = false; // disables animation defined by the 'x' property
window.linechart.options.transitions.active.animation.duration = 0; // disables the animation for 'active' mode
window.linechart.update();
}
function drawCPU(){
var newData = window.performanceData[window.performanceData?.length - 1];
$('#labelCPUUtilization').html(`${newData?.cpuPercentUtility?.toFixed(1)} %`);
$('#labelLogicalProc').html(`${newData?.logicalProcessorCount}`);
if(newData?.cpuSpeedMHz > 1000){
$('#labelCPUSpeed').html(`${(newData?.cpuSpeedMHz/1000.0)?.toFixed(2)} GHz`);
}
else{
$('#labelCPUSpeed').html(`${newData?.cpuSpeedMHz?.toFixed(2)} MHz`);
}
var dataGraph = window.performanceData.map((v,idx,arr)=> v?.cpuPercentUtility ?? 0);
//console.log(dataGraph);
var chart = window.linechart;
chart.data.datasets.forEach((dataset) => {
dataset.data = dataGraph;
});
chart.update();
}
var memchart = null;
function initMemory(){
var labels = new Array();
for(var i=0;i<60; i++){
labels.push(i);
}
const data = {
labels: labels,
datasets: [
{
label: '% Memory Usage',
data: new Array(60),
borderColor: 'blue',
backgroundColor: 'lightblue',
fill: true
}
]
};
const config = {
type: 'line',
data: data,
options: {
responsive: true,
plugins: {
title: {
display: true,
text: (ctx) => 'Memory Usage'
},
tooltip: {
mode: 'index'
},
legend:{
display: false
},
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
scales: {
x: {
beginAtZero: true,
ticks:{
display: false,
},
title: {
display: false,
text: ''
}
},
y: {
min: 0,
max: 100,
stacked: false,
beginAtZero: true,
ticks:{
display: false,
},
title: {
display: false,
text: '%'
}
}
}
}
};
window.memchart = new Chart(
$('#graph-mem')[0],
config
);
window.memchart.options.animation = false; // disables all animations
window.memchart.options.animations.colors = false; // disables animation defined by the collection of 'colors' properties
window.memchart.options.animations.x = false; // disables animation defined by the 'x' property
window.memchart.options.transitions.active.animation.duration = 0; // disables the animation for 'active' mode
window.memchart.update();
}
function drawMemory(){
var newData = window.performanceData[window.performanceData?.length - 1];
if(newData?.memoryTotalMB > 1000){
$('#labelMemTotal').html(`${(newData?.memoryTotalMB/1024.0)?.toFixed(1)} GB`);
$('#labelMemAvailable').html(`${(newData?.memoryAvailableMB/1024.0)?.toFixed(1)} GB`);
$('#labelMemInUse').html(`${(newData?.memoryUsageMB/1024.0)?.toFixed(1)} GB (${(newData?.memoryUsagePercent)?.toFixed(0)} %)`);
}
else{
$('#labelMemTotal').html(`${newData?.memoryTotalMB?.toFixed(1)} MB`);
$('#labelMemAvailable').html(`${newData?.memoryAvailableMB?.toFixed(1)} MB`);
$('#labelMemInUse').html(`${newData?.memoryUsageMB?.toFixed(1)} MB (${(newData?.memoryUsagePercent)?.toFixed(0)} %)`);
}
var dataGraph = window.performanceData.map((v,idx,arr)=> v?.memoryUsagePercent ?? 0);
//console.log(dataGraph);
var chart = window.memchart;
chart.data.datasets.forEach((dataset) => {
dataset.data = dataGraph;
});
chart.update();
}
</script>
Langkah terakhir adalah mengubah kode pada Program.cs untuk mendukung service service yang diperlukan terkait signalR dan memapping Hub ke endpoint signalR. Buka file Program.cs dan ubah kode program menjadi seperti berikut
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddResourceMonitoring();
builder.Services.AddMvc();
builder.Services.AddControllersWithViews().AddRazorRuntimeCompilation();
builder.Services.AddSignalR();
builder.Services.AddSession();
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = new[]
{
"text/css",
"application/javascript",
"application/json",
"text/json",
"application/xml",
"text/xml",
"text/plain",
"image/svg+xml",
"application/x-font-ttf"
};
});
builder.Services.AddScoped<CPUMetricReaderFactory>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseResponseCompression();
app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
var cacheMaxAge = (60 * 60 * 24).ToString();
ctx.Context.Response.Headers.Append(
"Cache-Control", $"public, max-age={cacheMaxAge}");
}
});
app.UseRouting();
app.UseSession();
app.UseAuthorization();
app.MapStaticAssets();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.WithStaticAssets();
app.MapHub<PerformanceHub>("/performance");
app.Run();
}
}
Kita harus menambahkan kode baris builder.Services.AddSignalR() agar supaya aplikasi mendukung service signalR. Bagian baris kode app.MapHub(“/performance”) ditujukan agar hub PerformanceHub dapat diakses dari endpoint /performance. Dan baris kode builder.Services.AddScoped<CPUMetricReaderFactory>() merupakan dependency injection agar class CPUMetricReaderFactory dapat langsung diakses dari constructor class yang membutuhkan.
Apabila kita lihat, maka structure project di solution explorer akan menjadi seperti pada Gambar 11 berikut ini

Mari kita jalankan website dengan cara menekan tombol F5 di visual studio atau dari menu Debug. Berikut tampilan dashboard di aplikasi



Jika pada browser terdapat developer tools, kita bisa membuka developer tools dan melihat console developer tools untuk menampilkan object json yang diterima dari server.

Demikian langkah langkah membuat dashboard realtime menggunakan Signal R. Untuk listing kode lengkap Bisa diunduh di alamat berikut ini (DISINI).


