0

My .NET BackgroundService application have multple workers and all of them connects to the same SignalR Hub. Each worker gets a unique ConnectionId which will not work in my case.

How, if possible, can I share the HubConnection between all of the workers to prevent different ConnectionIds?

Jason Pan
  • 15,263
  • 1
  • 14
  • 29
linusdev
  • 11
  • 3

2 Answers2

1

I found a solution that solves my issue. I've created a HubService which every worker dependency inject and then calls EnsureConnectionEstablished() to make sure the connection is established.

public class HubService
{
        private readonly ILogger<SystemHealthWorker> _logger;
        private readonly ConfigurationService _config;
        private HubConnection _connection;

        public HubService(ILogger<SystemHealthWorker> logger, ConfigurationService configService)
        {
            _logger = logger;
            _config = configService;
        }

        public async Task<bool> EnsureConnectionEstablished()
        {
            _logger.LogDebug("Ensures that the hub connection is established.");

            if (_connection == null || _connection.State == HubConnectionState.Disconnected)
            {
                _logger.LogDebug("Hub connection is not established.");

                var config = await _config.GetConfiguration();
                if (config == null || string.IsNullOrEmpty(config.ServerAddress?.ToString()))
                {
                    throw new Exception("Invalid config. Hub connection will not be established.");
                }
                else if (string.IsNullOrWhiteSpace(config.AccessToken))
                {
                    throw new Exception("Invalid access token.");
                }

                var hubUrl = new Uri(config.ServerAddress);
                _logger.LogInformation($"Connecting to hub ({hubUrl})");

                _logger.LogDebug("Building hub connection.");
                _connection = new HubConnectionBuilder()
                .WithUrl(hubUrl, options => { options.AccessTokenProvider = () => Task.FromResult(config.AccessToken); })
                .WithAutomaticReconnect(new[] { TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(120) })
                .Build();

                _connection.Reconnecting += OnReconnecting;
                _connection.Reconnecting += OnReconnected;
                _connection.Closed += OnClosed;

                await _connection.StartAsync();

                _logger.LogInformation($"Connected to hub with id {_connection.ConnectionId}.");
            }

            return _connection?.State == HubConnectionState.Connected;
        }
 }
linusdev
  • 11
  • 3
0

From your description, it appears that each of your workers is connected to SignalR as a client. So the generated ConnectionId are different, from the actual development, it is also like this, a real user, may open multiple web pages to establish a connection with the SignalR Hub.

We usually need to use ConcurrentDictionary to save information about connected users. For more details, you can check my answer in this thread.

The different is you are using C# client, that is using javascript client. But managing connected users in Signalr Hub, this is consistent.

public partial class MainHub : Hub
{
    public static ConcurrentDictionary<string?, List<string>>? ConnectedUsers;

    public MainHub()
    {
        ConnectedUsers  = new ConcurrentDictionary<string?, List<string>>();
    }

    public override async Task OnConnectedAsync()
    {

        //// Get HttpContext In asp.net core signalr
        //IHttpContextFeature hcf = (IHttpContextFeature)this.Context.Features[typeof(IHttpContextFeature)];
        //HttpContext hc = hcf.HttpContext;
        //string uid = hc.Request.Path.Value.ToString().Split(new string[] { "/", "" }, StringSplitOptions.RemoveEmptyEntries)[1].ToString();

        string? userid = Context.User?.Identity?.Name;
        if (userid == null || userid.Equals(string.Empty))
        {
            Trace.TraceInformation("user not loged in, can't connect signalr service");
            return;
        }
        

        Trace.TraceInformation(userid + "connected");
        // save connection
        List<string>? existUserConnectionIds;
        ConnectedUsers.TryGetValue(userid, out existUserConnectionIds);
        if (existUserConnectionIds == null)
        {
            existUserConnectionIds = new List<string>();
        }
        existUserConnectionIds.Add(Context.ConnectionId);
        ConnectedUsers.TryAdd(userid, existUserConnectionIds);

        //await Clients.All.SendAsync("ServerInfo", userid, userid + " connected, connectionId = " + Context.ConnectionId);
        await base.OnConnectedAsync();
    }
}
Jason Pan
  • 15,263
  • 1
  • 14
  • 29
  • Yes, each worker connect to the hub as a client. I save the ConnectionId to the database and pair it with the User through the JWT-token when connecting to the hub. The problem with this is that the ConnectionId will be overwritten for every worker that connects. So if I register a handler in Worker 1 (this Worker's ConnectionId is overwritten in the database by later connected workers), this will never be invoked since I use the Id (which is from another worker without the registred handler) in database to invoke the method on the specific client. – linusdev Jul 12 '23 at 16:12
  • @linusdev You should store the userid instead of the connectionId, that's what I mean in my answer. – Jason Pan Jul 13 '23 at 11:46
  • I've solved the issue, added an answer. Thanks for your input! – linusdev Jul 13 '23 at 19:25