diff --git a/samples/ChatSample/PresenceHubLifetimeManager.cs b/samples/ChatSample/PresenceHubLifetimeManager.cs index 9c41231145..db881b2d0c 100644 --- a/samples/ChatSample/PresenceHubLifetimeManager.cs +++ b/samples/ChatSample/PresenceHubLifetimeManager.cs @@ -186,5 +186,10 @@ namespace ChatSample { return _wrappedHubLifetimeManager.InvokeGroupExceptAsync(groupName, methodName, args, excludedIds); } + + public override Task InvokeUsersAsync(IReadOnlyList userIds, string methodName, object[] args) + { + return _wrappedHubLifetimeManager.InvokeUsersAsync(userIds, methodName, args); + } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs b/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs index f76003621d..f924aee91f 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs @@ -208,5 +208,13 @@ namespace Microsoft.AspNetCore.SignalR return connectionIds.Contains(connection.ConnectionId); }); } + + public override Task InvokeUsersAsync(IReadOnlyList userIds, string methodName, object[] args) + { + return InvokeAllWhere(methodName, args, connection => + { + return userIds.Contains(connection.UserIdentifier); + }); + } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/DynamicHubClients.cs b/src/Microsoft.AspNetCore.SignalR.Core/DynamicHubClients.cs index 8c4744340d..e18b01f531 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/DynamicHubClients.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/DynamicHubClients.cs @@ -25,5 +25,6 @@ namespace Microsoft.AspNetCore.SignalR public dynamic OthersInGroup(string groupName) => new DynamicClientProxy(_clients.OthersInGroup(groupName)); public dynamic Others => new DynamicClientProxy(_clients.Others); public dynamic User(string userId) => new DynamicClientProxy(_clients.User(userId)); + public dynamic Users(IReadOnlyList users) => new DynamicClientProxy(_clients.Users(users)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubCallerClients.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubCallerClients.cs index 95044ae4c6..7c6f109732 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubCallerClients.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubCallerClients.cs @@ -63,5 +63,10 @@ namespace Microsoft.AspNetCore.SignalR { return _hubClients.Clients(connectionIds); } + + public IClientProxy Users(IReadOnlyList userIds) + { + return _hubClients.Users(userIds); + } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubClients.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubClients.cs index afb1f06bf1..d24d75d925 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubClients.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubClients.cs @@ -51,5 +51,10 @@ namespace Microsoft.AspNetCore.SignalR { return new UserProxy(_lifetimeManager, userId); } + + public IClientProxy Users(IReadOnlyList userIds) + { + return new MultipleUserProxy(_lifetimeManager, userIds); + } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubClients`T.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubClients`T.cs index b3337119e6..641b7d0675 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubClients`T.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubClients`T.cs @@ -1,9 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; -using System.Text; namespace Microsoft.AspNetCore.SignalR { @@ -53,5 +51,10 @@ namespace Microsoft.AspNetCore.SignalR { return TypedClientBuilder.Build(new UserProxy(_lifetimeManager, userId)); } + + public virtual T Users(IReadOnlyList userIds) + { + return TypedClientBuilder.Build(new MultipleUserProxy(_lifetimeManager, userIds)); + } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubLifetimeManager.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubLifetimeManager.cs index 54693fbdc1..79e83e5eb1 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubLifetimeManager.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubLifetimeManager.cs @@ -28,6 +28,8 @@ namespace Microsoft.AspNetCore.SignalR public abstract Task InvokeUserAsync(string userId, string methodName, object[] args); + public abstract Task InvokeUsersAsync(IReadOnlyList userIds, string methodName, object[] args); + public abstract Task AddGroupAsync(string connectionId, string groupName); public abstract Task RemoveGroupAsync(string connectionId, string groupName); diff --git a/src/Microsoft.AspNetCore.SignalR.Core/IHubClients`T.cs b/src/Microsoft.AspNetCore.SignalR.Core/IHubClients`T.cs index 8fd399ff6e..3b4c4ded9a 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/IHubClients`T.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/IHubClients`T.cs @@ -22,6 +22,8 @@ namespace Microsoft.AspNetCore.SignalR T GroupExcept(string groupName, IReadOnlyList excludeIds); T User(string userId); + + T Users(IReadOnlyList userIds); } } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/Proxies.cs b/src/Microsoft.AspNetCore.SignalR.Core/Proxies.cs index e5a9c71bea..30ed179c54 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/Proxies.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/Proxies.cs @@ -23,6 +23,23 @@ namespace Microsoft.AspNetCore.SignalR } } + public class MultipleUserProxy : IClientProxy + { + private readonly IReadOnlyList _userIds; + private readonly HubLifetimeManager _lifetimeManager; + + public MultipleUserProxy(HubLifetimeManager lifetimeManager, IReadOnlyList userIds) + { + _lifetimeManager = lifetimeManager; + _userIds = userIds; + } + + public Task InvokeAsync(string method, params object[] args) + { + return _lifetimeManager.InvokeUsersAsync(_userIds, method, args); + } + } + public class GroupProxy : IClientProxy { private readonly string _groupName; diff --git a/src/Microsoft.AspNetCore.SignalR.Core/TypedHubClients.cs b/src/Microsoft.AspNetCore.SignalR.Core/TypedHubClients.cs index 76768bbdc6..0ac92057f6 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/TypedHubClients.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/TypedHubClients.cs @@ -56,5 +56,10 @@ namespace Microsoft.AspNetCore.SignalR { return TypedClientBuilder.Build(_hubClients.User(userId)); } + + public T Users(IReadOnlyList userIds) + { + return TypedClientBuilder.Build(_hubClients.Users(userIds)); + } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs b/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs index 8ac4d40fa1..49acdb7d1d 100644 --- a/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs +++ b/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs @@ -621,6 +621,26 @@ namespace Microsoft.AspNetCore.SignalR.Redis return Task.WhenAll(publishTasks); } + public override Task InvokeUsersAsync(IReadOnlyList userIds, string methodName, object[] args) + { + if (userIds.Count > 0) + { + var message = new RedisInvocationMessage(methodName, args); + var publishTasks = new List(userIds.Count); + foreach (var userId in userIds) + { + if (!string.IsNullOrEmpty(userId)) + { + publishTasks.Add(PublishAsync(_channelNamePrefix + ".user." + userId, message)); + } + } + + return Task.WhenAll(publishTasks); + } + + return Task.CompletedTask; + } + private class LoggerTextWriter : TextWriter { private readonly ILogger _logger; diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTestUtils/Hubs.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTestUtils/Hubs.cs index 299dec8012..0bb86bdbf6 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTestUtils/Hubs.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTestUtils/Hubs.cs @@ -22,6 +22,11 @@ namespace Microsoft.AspNetCore.SignalR.Tests.HubEndpointTestUtils return Clients.User(userId).InvokeAsync("Send", message); } + public Task SendToMultipleUsers(IReadOnlyList userIds, string message) + { + return Clients.Users(userIds).InvokeAsync("Send", message); + } + public Task ConnectionSendMethod(string connectionId, string message) { return Clients.Client(connectionId).InvokeAsync("Send", message); @@ -181,6 +186,11 @@ namespace Microsoft.AspNetCore.SignalR.Tests.HubEndpointTestUtils return Clients.User(userId).Send(message); } + public Task SendToMultipleUsers(IReadOnlyList userIds, string message) + { + return Clients.Users(userIds).Send(message); + } + public Task ConnectionSendMethod(string connectionId, string message) { return Clients.Client(connectionId).Send(message); @@ -256,6 +266,11 @@ namespace Microsoft.AspNetCore.SignalR.Tests.HubEndpointTestUtils return Clients.User(userId).Send(message); } + public Task SendToMultipleUsers(IReadOnlyList userIds, string message) + { + return Clients.Users(userIds).Send(message); + } + public Task ConnectionSendMethod(string connectionId, string message) { return Clients.Client(connectionId).Send(message); diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs index ad73f122ac..5fc80a9569 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs @@ -956,8 +956,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests var secondAndThirdClients = new HashSet {secondClient.Connection.ConnectionId, thirdClient.Connection.ConnectionId }; - secondAndThirdClients.Add(secondClient.Connection.ConnectionId); - await firstClient.SendInvocationAsync("SendToMultipleClients", "Second and Third", secondAndThirdClients).OrTimeout(); var secondClientResult = await secondClient.ReadAsync().OrTimeout(); @@ -984,6 +982,51 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Theory] + [MemberData(nameof(HubTypes))] + public async Task SendToMultipleUsers(Type hubType) + { + dynamic endPoint = HubEndPointTestUtils.GetHubEndpoint(hubType); + + using (var firstClient = new TestClient(addClaimId: true)) + using (var secondClient = new TestClient(addClaimId: true)) + using (var thirdClient = new TestClient(addClaimId: true)) + { + Task firstEndPointTask = endPoint.OnConnectedAsync(firstClient.Connection); + Task secondEndPointTask = endPoint.OnConnectedAsync(secondClient.Connection); + Task thirdEndPointTask = endPoint.OnConnectedAsync(thirdClient.Connection); + + await Task.WhenAll(firstClient.Connected, secondClient.Connected, thirdClient.Connected).OrTimeout(); + + var secondAndThirdClients = new HashSet {secondClient.Connection.User.FindFirst(ClaimTypes.NameIdentifier)?.Value, + thirdClient.Connection.User.FindFirst(ClaimTypes.NameIdentifier)?.Value }; + + await firstClient.SendInvocationAsync(nameof(MethodHub.SendToMultipleUsers), secondAndThirdClients, "Second and Third").OrTimeout(); + + var secondClientResult = await secondClient.ReadAsync().OrTimeout(); + var invocation = Assert.IsType(secondClientResult); + Assert.Equal("Send", invocation.Target); + Assert.Equal("Second and Third", invocation.Arguments[0]); + + var thirdClientResult = await thirdClient.ReadAsync().OrTimeout(); + invocation = Assert.IsType(thirdClientResult); + Assert.Equal("Send", invocation.Target); + Assert.Equal("Second and Third", invocation.Arguments[0]); + + // Check that first client only got the completion message + var hubMessage = await firstClient.ReadAsync().OrTimeout(); + Assert.IsType(hubMessage); + Assert.Null(firstClient.TryRead()); + + // kill the connections + firstClient.Dispose(); + secondClient.Dispose(); + thirdClient.Dispose(); + + await Task.WhenAll(firstEndPointTask, secondEndPointTask, thirdEndPointTask).OrTimeout(); + } + } + [Theory] [MemberData(nameof(HubTypes))] public async Task HubsCanAddAndSendToGroup(Type hubType)