исключение c # .NET Core SignalR при вызове клиентом функции (не всегда происходит)

#c# #signalr #signalr-hub #asp.net-core-signalr

#c# #signalr #signalr-концентратор #asp.net-core-signalr

Вопрос:

У меня есть следующий серверный SignalR концентратор:

  public class AlertHub : Hub
    {
        static ConcurrentDictionary<string, string> _users = new ConcurrentDictionary<string, string>();

        public Task SendMessage(string user, string message)
        {
            return Clients.All.SendAsync("BroadcastMessage", user, message);
        }

        public Task SendMessageToUser(string userUid, string user, string message, string totalMessages)
        {
            var clientId = _users.FirstOrDefault(x => x.Value == userUid).Key;
            return Clients.Client(clientId).SendAsync("BroadcastMessage", user, message, totalMessages);
        }

        public static void ClearState()
        {
            _users.Clear();
        }

        public override Task OnConnectedAsync()
        {
            _users.TryAdd(Context.ConnectionId, Context.ConnectionId);

            return base.OnConnectedAsync();
        }

        public override Task OnDisconnectedAsync(Exception exception)
        {
            string userUid;
            _users.TryRemove(Context.ConnectionId, out userUid);

            return base.OnDisconnectedAsync(exception);
        }

        public void SetUserUid(string userUid)
        {
            _users[Context.ConnectionId] = userUid;
        }
    }
  

На моем клиенте непосредственно перед await _signalRConnection.StartAsync(); тем, как я установил userUid в свой _users словарь:

 //Send user name for this client, so we won't need to send it with every message
                await _signalRConnection.InvokeAsync("SetUserUid", this.State.UserUID);
  

И это работает идеально, без исключений.

Затем на моем клиенте, когда я пытаюсь отправить сообщение, я делаю следующее: (это обычно работает, исключение прерывистое)

 await _signalRConnection.InvokeAsync("SendMessageToUser", model.UserUid.ToString().ToLower(), this.State.UserName, $"{model.UserName} has sent you a message.", countMessagesString);
  

Обычно это работает, но иногда он начал аварийно завершать работу, показывая на сервере следующую ошибку:

 Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher.? [?] - MESSAGE: Failed to invoke hub method 'SendMessageToUser'.
 System.ArgumentNullException: Value cannot be null. (Parameter 'connectionId')
  

Это связано с тем, что при попытке получить идентификатор клиента он возвращает здесь null:

  var clientId = _users.FirstOrDefault(x => x.Value == userUid).Key; <-- HERE
return Clients.Client(clientId).SendAsync("BroadcastMessage", user, message, totalMessages);
  

Поэтому я не знаю, почему в _users словаре иногда не будет ключа. Это довольно странно.

Есть какие-либо подсказки о том, как разобраться или решить эту проблему?

** ОБНОВИТЬ **

Это то, что я сделал:

   public class AlertHubDictionary
    {
        static ConcurrentDictionary<string, string> _users;

        public AlertHubDictionary()
        {
            _users = new ConcurrentDictionary<string, string>();
        }

        public ConcurrentDictionary<string, string> UsersDictionary { get { return _users; } }

        public void ClearUsersDictionary()
        {
            _users.Clear();
        }
    }
  

Затем этот класс зарегистрирован как одноэлементный DI:

    services.AddSingleton<AlertHubDictionary>();
  

И, наконец, выполните замены в классе Hub:

 public class AlertHub : Hub
    {
        private readonly ILogger<AlertHub> _logger;
        private AlertHubDictionary _hubDictionary;
        static ConcurrentDictionary<string, string> _users = new ConcurrentDictionary<string, string>();

        public AlertHub(ILogger<AlertHub> logger, AlertHubDictionary hubDictionary)
        {
            _logger = logger;
            _hubDictionary = hubDictionary;
        }
        public Task SendMessage(string user, string message)
        {
            //Clients.Client("").SendAsync("BroadcastMessage", user, message);
            return Clients.All.SendAsync("BroadcastMessage", user, message);
        }

        public Task SendMessageToUser(string userUid, string user, string message, string totalMessages)
        {
            //var clientId = _users.FirstOrDefault(x => x.Value == userUid).Key;
            var clientId = _hubDictionary.UsersDictionary.FirstOrDefault(x => x.Value == userUid).Key;

            using (var scope = _logger.BeginScope("SCOPED_VALUE"))
            {
                _logger.LogInformation($"Send message to userUid {userUid} - {user} with clientId {clientId}.");

            }


            if (clientId == null)
            {
                // the user is not connected
                return Task.FromResult(0);
            }
            else
            {

                return Clients.Client(clientId).SendAsync("BroadcastMessage", user, message, totalMessages);
            }
        }

        public void ClearState()
        {
            _hubDictionary.ClearUsersDictionary();
            //_users.Clear();

            using (var scope = _logger.BeginScope("SCOPED_VALUE"))
            {
                _logger.LogInformation("_users cleared");

            }

        }

        public override Task OnConnectedAsync()
        {
            _hubDictionary.UsersDictionary.TryAdd(Context.ConnectionId, Context.ConnectionId);
            //_users.TryAdd(Context.ConnectionId, Context.ConnectionId);

            using (var scope = _logger.BeginScope("SCOPED_VALUE"))
            {
                _logger.LogInformation($"ConnectionId {Context.ConnectionId} connected.");

            }

            return base.OnConnectedAsync();
        }

        public override Task OnDisconnectedAsync(Exception exception)
        {
            string userUid;
            _hubDictionary.UsersDictionary.TryRemove(Context.ConnectionId, out userUid);

            using (var scope = _logger.BeginScope("SCOPED_VALUE"))
            {
                _logger.LogInformation($"UserUid {userUid} disconnected.");

            }


            return base.OnDisconnectedAsync(exception);
        }

        public void SetUserUid(string userUid)
        {
            _hubDictionary.UsersDictionary[Context.ConnectionId] = userUid;

            using (var scope = _logger.BeginScope("SCOPED_VALUE"))
            {
                _logger.LogInformation($"Connection {Context.ConnectionId} linked to userUid {userUid}");

            }

            //ClientNameChanged?.Invoke(Context.ConnectionId, userName);
        }
    }
  

Комментарии:

1. Поместите запись в файл. Регистрируйте методы, в которых _users добавлено / удалено. Вероятно, вы время от времени теряете соединение = вот почему оно очищается.

Ответ №1:

Судя по тому, как вы фиксируете идентификаторы пользователей, похоже, что вы пытаетесь обработать концентратор SignalR как одноэлементный. Согласно документации Microsoft, концентратор является переходным классом:

https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-3.1#create-and-use-hubs

Концентраторы являются временными:

Не сохраняйте состояние в свойстве класса hub. Каждый вызов метода концентратора выполняется в новом экземпляре концентратора.

Таким образом, если вы хотите сохранить общий экземпляр ваших зарегистрированных идентификаторов пользователей, вам, вероятно, следует управлять им как отдельным классом, зарегистрированным как одноэлементный и введенным в ваш класс hub, чтобы состояние можно было поддерживать независимо от hub.

Комментарии:

1. Потрясающе. Да, это имеет смысл. Любая ссылка, по которой я могу посмотреть, как ее реализовать?

2. @VAAA — Вы могли бы сделать это так же просто, как переместить этот словарь в его собственный класс, зарегистрировать его при запуске service.AddSingleton<YourUserCacheClass>() , а затем ввести его в конструктор концентратора, используя внедрение конструктора, как описано в learn.microsoft.com/en-us/aspnet/core/fundamentals /…

3. Здесь есть очень похожий пример автономного класса от Microsoft, хотя и помеченный как внутренний, тогда как вас устроит public: github.com/dotnet/extensions/blob/release/3.1/src /…

4. Большое спасибо, я попробую

5. Я только что обновил внесенное мной изменение, я собираюсь попробовать.