How to fine tune SignalR connection in Blazor Server?

Cenk 1,021 Reputation points
2024-11-14T17:03:55.79+00:00

Hello there,

I am trying to read data from a weight measurement device and send data to a server instantly with SignalR. The architecture consists of a console application that collects the measurement data and an ASP.NET Core API with a hub that distributes the data to connected clients (Blazor Server).

I have implemented this setup and tested it on my local machine. However, since I am not very familiar with SignalR, I believe there are several areas that require refactoring. Feedback on my code would be greatly appreciated.

One issue I have noticed is that the OnInitializedAsync method in App.razor is invoked twice when starting the Blazor Server application:

Initializing app... Connecting to the hub... BaseUri: https://localhost:7008 Connected to the hub successfully! App initialized successfully! Event handler removed successfully. Initializing app... Connecting to the hub... BaseUri: https://localhost:7008 Connected to the hub successfully! App initialized successfully!

Here are the relevant code snippets for the Blazor server application:

App.razor

@using WeighingDeviceDashboard.Service
@inject WeighingHubService WeighingHubService
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
@code {
    private bool _isInitialized = false;
    protected override async Task OnInitializedAsync()
    {
        if (!_isInitialized)
        {
            Console.WriteLine("Initializing app...");
            await WeighingHubService.Connect();
            Console.WriteLine("App initialized successfully!");
            _isInitialized = true;
        }
        else
        {
            Console.WriteLine("App already initialized.");
        }
    }
}


Program.cs

using WeighingDeviceDashboard.Service;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<WeighingHubService>(sp => new WeighingHubService(
    sp.GetService<IConfiguration>()));
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();

WeighingHubService.cs

using Microsoft.AspNetCore.SignalR.Client;
namespace WeighingDeviceDashboard.Service
{
    public class WeighingHubService
    {
        private readonly IConfiguration _configuration;
        private HubConnection _hubConnection;
        private bool _isConnected = false;
        private readonly object _connectionLock = new object();
        public WeighingHubService(IConfiguration? configuration)
        {
            _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
        }
        public async Task Connect()
        {
            lock (_connectionLock)
            {
                if (_isConnected)
                {
                    Console.WriteLine("Already connected to the hub.");
                    return;
                }
            }
            try
            {
                Console.WriteLine("Connecting to the hub...");
                _hubConnection = new HubConnectionBuilder()
                    .WithUrl($"{GetBaseUri()}/weighingHub")
                    .Build();
                _hubConnection.On<Measurement>("ReceiveMeasurement", async (measurement) =>
                {
                    if (MeasurementReceived != null)
                    {
                        await MeasurementReceived.Invoke(measurement);
                    }
                });
                await _hubConnection.StartAsync();
                Console.WriteLine("Connected to the hub successfully!");
                lock (_connectionLock)
                {
                    _isConnected = true;
                }
            }
            catch (Exception ex)
            {
                // Log the exception or handle it as needed
                Console.WriteLine($"Error connecting to the hub: {ex.Message}");
                //throw;
            }
        }
        public event Func<Measurement, Task> MeasurementReceived;
        private string GetBaseUri()
        {
            var baseUri = _configuration["BaseUri"];
            Console.WriteLine($"BaseUri: {baseUri}");
            if (string.IsNullOrEmpty(baseUri))
            {
                throw new InvalidOperationException("BaseUri configuration is missing or empty.");
            }
            return baseUri;
        }
    }
}


Index.razor

@page "/"
@using WeighingDeviceDashboard.Service
@inject WeighingHubService WeighingHubService
@implements IDisposable
<h1>Weighing Device Dashboard</h1>
@if (string.IsNullOrEmpty(_currentMeasurement))
{
    <p>No measurements received yet.</p>
}
else
{
    <p>Current Measurement: @_currentMeasurement</p>
}
@code {
    private string _currentMeasurement = "";
    private bool _isSubscribed = false;
    private Func<Measurement, Task> _measurementReceivedHandler;
    protected override async Task OnInitializedAsync()
    {
        if (!_isSubscribed)
        {
            _measurementReceivedHandler = async (measurement) =>
            {
                if (measurement != null)
                {
                    try
                    {
                        Console.WriteLine($"Received measurement: {measurement.Value} {measurement.Unit}");
                        _currentMeasurement = $"{measurement.Value} {measurement.Unit}";
                        await InvokeAsync(StateHasChanged);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Error updating measurement: {ex.Message}");
                    }
                }
                else
                {
                    Console.WriteLine("Received null measurement.");
                }
            };
            WeighingHubService.MeasurementReceived += _measurementReceivedHandler;
            _isSubscribed = true;
        }
    }
    public void Dispose()
    {
        if (_isSubscribed && _measurementReceivedHandler != null)
        {
            WeighingHubService.MeasurementReceived -= _measurementReceivedHandler;
            _isSubscribed = false;
            _measurementReceivedHandler = null;
            Console.WriteLine("Event handler removed successfully.");
        }
        else
        {
            Console.WriteLine("Event handler removal failed. Check for null references or subscription status.");
        }
    }
}
ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,639 questions
Blazor
Blazor
A free and open-source web framework that enables developers to create web apps using C# and HTML being developed by Microsoft.
1,602 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
11,053 questions
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Bruce (SqlWork.com) 67,251 Reputation points
    2024-11-14T18:09:04.74+00:00

    Blazor server uses a signal/r hub to communicate to the client code in the browser. so your app has 2 hubs, one for the blazor app and one for the measurement service.

    each Blazor app instance will have two server side signal/r clients, one to connect to the blazor hub, and one to connect to the measure hub. in addition there will be a browser signal/r client to connect to the blazor hub.

    its not clear why the code connects to the measure hub at startup, and not on the page that actually uses the hub. do all blazor pages use the measure hub? if not why do you want measure events sent to a blazor instance that is ignoring them.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.