I have an app where I want to centralise a notification service. With hardware sending out updates of it current status as it changes.
There are a number of separate components which need to react to the current status of the hardware, which would just look to hook into the OnStateChanged method and then do what they need to do.
This is my client service:
public class SystemStateClientService : IAsyncDisposable
{
private readonly HubConnection _hubConnection;
private bool _started = false;
private CurrentSystemState? _currentState = null;
public event Action<CurrentSystemState>? OnStateChanged;
public SystemStateClientService(NavigationManager navigation)
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(navigation.ToAbsoluteUri("/hubs/notification"))
.WithAutomaticReconnect()
.Build();
}
public async Task StartAsync()
{
if (_started)
return;
_hubConnection.On<string>("SystemStateChanged", (newState) =>
{
if (Enum.TryParse(newState, out CurrentSystemState parsed))
{
_currentState = parsed;
OnStateChanged?.Invoke(parsed);
}
});
try
{
if (_hubConnection.State == HubConnectionState.Disconnected)
{
await _hubConnection.StartAsync();
Console.WriteLine("✅ SignalR connection started.");
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ Failed to start SignalR connection: {ex.Message}");
}
_started = true;
}
public async Task<CurrentSystemState?> GetCurrentStateAsync()
{
if (_currentState != null)
return _currentState;
try
{
var currentStateStr = await _hubConnection.InvokeAsync<string>("GetCurrentSystemState");
if (Enum.TryParse(currentStateStr, out CurrentSystemState parsed))
{
_currentState = parsed;
OnStateChanged?.Invoke(parsed);
return parsed;
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ Failed to fetch current system state from server: {ex.Message}");
}
return null;
}
public async ValueTask DisposeAsync()
{
await _hubConnection.DisposeAsync();
}
}
Ideally I would like to mark this as singleton, but as it is using the SignalR hub it will only allow scoped injection. So in my program.cs I have:
builder.Services.AddScoped<SystemStateClientService>();
Then I try to inject this into my MainLayout.razor:
@inherits LayoutComponentBase
@inject SystemStateClientService SystemStateClient
<div class="page">
<main>
<div class="top-row px-4">
<NavMenu />
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@code {
private bool _initialized = false;
protected override async Task OnInitializedAsync()
{
if (!_initialized)
{
await SystemStateClient.StartAsync();
_initialized = true;
}
}
}
I just use
@inject SystemStateClientService SystemStateClient
in the .razor pages and components I want to cascade this down into.
The issue is that the scoped service is being recycled and .StartAsync has not been triggered and so the _hubConnection in turn is then not open?
I want to avoid having each component opening a connection for the same events.
Can anyone help here? Or do you have any other ideas for a solution?
CircuitHandlerclass to wrap the service and inject it into the razor pages. But credit to you for the wrapper idea and responses.