1

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?

4
  • 1
    You could create a factory that builds/keeps a handle to your hub and register the DI factory as a singleton - you'd need to be careful with things like AccessTokenProviders and the lifetime of the hub itself though Commented Jun 25 at 9:23
  • 1
    Ok so effectively a wrapper which would just call the start method? But then this would require the async method to be triggered in the contructor wouldn't it? so we are back in the same scenario? Commented Jun 25 at 9:39
  • 1
    Not quite the same scenario as you've abstracted the hub to a singleton which is the first part of your question. the async initialisation is a headache - there are ways of doing it : blog.stephencleary.com/2013/01/async-oop-2-constructors.html - however the way I've overcome this in projects is to just wrap the (async) methods on the hub and call an EnsureConnected() method - this way the hub itself just grabs a connection if it needs it within the async method/context. HTH Commented Jun 25 at 9:56
  • 1
    @NickPattman Thank you for that - I managed to come up with a working solution. I used the CircuitHandler class to wrap the service and inject it into the razor pages. But credit to you for the wrapper idea and responses. Commented Jun 25 at 10:18

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.