diff --git a/src/Components/Server/test/Circuits/ComponentHubTest.cs b/src/Components/Server/test/Circuits/ComponentHubTest.cs index 98031f456069..9bbd9b17ff76 100644 --- a/src/Components/Server/test/Circuits/ComponentHubTest.cs +++ b/src/Components/Server/test/Circuits/ComponentHubTest.cs @@ -85,7 +85,7 @@ public async Task CannotInvokeOnLocationChangedBeforeInitialization() mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny()), Times.Once()); } - private static (Mock, ComponentHub) InitializeComponentHub() + private static (Mock, ComponentHub) InitializeComponentHub() { var ephemeralDataProtectionProvider = new EphemeralDataProtectionProvider(); var circuitIdFactory = new CircuitIdFactory(ephemeralDataProtectionProvider); @@ -112,7 +112,7 @@ private static (Mock, ComponentHub) InitializeComponentHub() // Here we mock out elements of the Hub that are typically configured // by SignalR as clients connect to the hub. var mockCaller = new Mock(); - var mockClientProxy = new Mock(); + var mockClientProxy = new Mock(); mockCaller.Setup(x => x.Caller).Returns(mockClientProxy.Object); hub.Clients = mockCaller.Object; var mockContext = new Mock(); diff --git a/src/SignalR/clients/ts/FunctionalTests/Startup.cs b/src/SignalR/clients/ts/FunctionalTests/Startup.cs index c9c307f66077..1d831c9d28c9 100644 --- a/src/SignalR/clients/ts/FunctionalTests/Startup.cs +++ b/src/SignalR/clients/ts/FunctionalTests/Startup.cs @@ -232,7 +232,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger< { try { - var result = await hubContext.Clients.Single(id).InvokeAsync("Result"); + var result = await hubContext.Clients.Client(id).InvokeAsync("Result"); return result.ToString(CultureInfo.InvariantCulture); } catch (Exception ex) diff --git a/src/SignalR/server/Core/src/IHubCallerClients.cs b/src/SignalR/server/Core/src/IHubCallerClients.cs index 82968013d23f..688591a37cef 100644 --- a/src/SignalR/server/Core/src/IHubCallerClients.cs +++ b/src/SignalR/server/Core/src/IHubCallerClients.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.SignalR.Internal; + namespace Microsoft.AspNetCore.SignalR; /// @@ -13,5 +15,11 @@ public interface IHubCallerClients : IHubCallerClients /// /// The connection ID. /// A client caller. - new ISingleClientProxy Single(string connectionId) => throw new NotImplementedException(); + new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubCallerClients)this).Client(connectionId), "IHubCallerClients.Client(string connectionId)"); + + /// + /// Gets a proxy that can be used to invoke methods on the calling client and receive results. + /// + /// A client caller. + new ISingleClientProxy Caller => new NonInvokingSingleClientProxy(((IHubCallerClients)this).Caller, "IHubCallerClients.Caller"); } diff --git a/src/SignalR/server/Core/src/IHubClients.cs b/src/SignalR/server/Core/src/IHubClients.cs index 3646d4bc8258..008b1df2d61f 100644 --- a/src/SignalR/server/Core/src/IHubClients.cs +++ b/src/SignalR/server/Core/src/IHubClients.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.SignalR.Internal; + namespace Microsoft.AspNetCore.SignalR; /// @@ -13,5 +15,5 @@ public interface IHubClients : IHubClients /// /// The connection ID. /// A client caller. - new ISingleClientProxy Single(string connectionId) => throw new NotImplementedException(); + new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubClients)this).Client(connectionId), "IHubClients.Client(string connectionId)"); } diff --git a/src/SignalR/server/Core/src/IHubClients`T.cs b/src/SignalR/server/Core/src/IHubClients`T.cs index 06dce7a609cd..b6cc0634bdeb 100644 --- a/src/SignalR/server/Core/src/IHubClients`T.cs +++ b/src/SignalR/server/Core/src/IHubClients`T.cs @@ -9,13 +9,6 @@ namespace Microsoft.AspNetCore.SignalR; /// The client invoker type. public interface IHubClients { - /// - /// Gets a that can be used to invoke methods on a single client connected to the hub and receive results. - /// - /// The connection ID. - /// A client caller. - T Single(string connectionId) => Client(connectionId); - /// /// Gets a that can be used to invoke methods on all clients connected to the hub. /// diff --git a/src/SignalR/server/Core/src/Internal/HubCallerClients.cs b/src/SignalR/server/Core/src/Internal/HubCallerClients.cs index dc1645f83d25..04ce2c784c16 100644 --- a/src/SignalR/server/Core/src/Internal/HubCallerClients.cs +++ b/src/SignalR/server/Core/src/Internal/HubCallerClients.cs @@ -18,37 +18,19 @@ public HubCallerClients(IHubClients hubClients, string connectionId, bool parall _parallelEnabled = parallelEnabled; } - private sealed class NotParallelSingleClientProxy : ISingleClientProxy + IClientProxy IHubCallerClients.Caller => Caller; + public ISingleClientProxy Caller { - private readonly ISingleClientProxy _proxy; - - public NotParallelSingleClientProxy(ISingleClientProxy hubClients) - { - _proxy = hubClients; - } - - public Task InvokeCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + get { - throw new InvalidOperationException("Client results inside a Hub method requires HubOptions.MaximumParallelInvocationsPerClient to be greater than 1."); - } - - public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) - { - return _proxy.SendCoreAsync(method, args, cancellationToken); + if (!_parallelEnabled) + { + return new NotParallelSingleClientProxy(_hubClients.Client(_connectionId)); + } + return _hubClients.Client(_connectionId); } } - public ISingleClientProxy Single(string connectionId) - { - if (!_parallelEnabled) - { - return new NotParallelSingleClientProxy(_hubClients.Single(connectionId)); - } - return _hubClients.Single(connectionId); - } - - public IClientProxy Caller => _hubClients.Client(_connectionId); - public IClientProxy Others => _hubClients.AllExcept(_currentConnectionId); public IClientProxy All => _hubClients.All; @@ -58,8 +40,13 @@ public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) return _hubClients.AllExcept(excludedConnectionIds); } - public IClientProxy Client(string connectionId) + IClientProxy IHubClients.Client(string connectionId) => Client(connectionId); + public ISingleClientProxy Client(string connectionId) { + if (!_parallelEnabled) + { + return new NotParallelSingleClientProxy(_hubClients.Client(connectionId)); + } return _hubClients.Client(connectionId); } @@ -97,4 +84,24 @@ public IClientProxy Users(IReadOnlyList userIds) { return _hubClients.Users(userIds); } + + private sealed class NotParallelSingleClientProxy : ISingleClientProxy + { + private readonly ISingleClientProxy _proxy; + + public NotParallelSingleClientProxy(ISingleClientProxy hubClients) + { + _proxy = hubClients; + } + + public Task InvokeCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + throw new InvalidOperationException("Client results inside a Hub method requires HubOptions.MaximumParallelInvocationsPerClient to be greater than 1."); + } + + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + return _proxy.SendCoreAsync(method, args, cancellationToken); + } + } } diff --git a/src/SignalR/server/Core/src/Internal/HubClients.cs b/src/SignalR/server/Core/src/Internal/HubClients.cs index 9ddaee52ebc2..943080388840 100644 --- a/src/SignalR/server/Core/src/Internal/HubClients.cs +++ b/src/SignalR/server/Core/src/Internal/HubClients.cs @@ -13,11 +13,6 @@ public HubClients(HubLifetimeManager lifetimeManager) All = new AllClientProxy(_lifetimeManager); } - public ISingleClientProxy Single(string connectionId) - { - return new SingleClientProxy(_lifetimeManager, connectionId); - } - public IClientProxy All { get; } public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) @@ -25,7 +20,8 @@ public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) return new AllClientsExceptProxy(_lifetimeManager, excludedConnectionIds); } - public IClientProxy Client(string connectionId) + IClientProxy IHubClients.Client(string connectionId) => Client(connectionId); + public ISingleClientProxy Client(string connectionId) { return new SingleClientProxy(_lifetimeManager, connectionId); } diff --git a/src/SignalR/server/Core/src/Internal/NonInvokingSingleClientProxy.cs b/src/SignalR/server/Core/src/Internal/NonInvokingSingleClientProxy.cs new file mode 100644 index 000000000000..4f45ad43f1ea --- /dev/null +++ b/src/SignalR/server/Core/src/Internal/NonInvokingSingleClientProxy.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.SignalR.Internal; + +internal sealed class NonInvokingSingleClientProxy : ISingleClientProxy +{ + private readonly IClientProxy _clientProxy; + private readonly string _memberName; + + public NonInvokingSingleClientProxy(IClientProxy clientProxy, string memberName) + { + _clientProxy = clientProxy; + _memberName = memberName; + } + + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) => + _clientProxy.SendCoreAsync(method, args, cancellationToken); + + public Task InvokeCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) => + throw new NotImplementedException($"The default implementation of {_memberName} does not support client return results."); +} diff --git a/src/SignalR/server/Core/src/Internal/TypedHubClients.cs b/src/SignalR/server/Core/src/Internal/TypedHubClients.cs index fa7023ac332b..723d09e7fbd8 100644 --- a/src/SignalR/server/Core/src/Internal/TypedHubClients.cs +++ b/src/SignalR/server/Core/src/Internal/TypedHubClients.cs @@ -12,7 +12,7 @@ public TypedHubClients(IHubCallerClients dynamicContext) _hubClients = dynamicContext; } - public T Client(string connectionId) => TypedClientBuilder.Build(_hubClients.Single(connectionId)); + public T Client(string connectionId) => TypedClientBuilder.Build(_hubClients.Client(connectionId)); public T All => TypedClientBuilder.Build(_hubClients.All); diff --git a/src/SignalR/server/Core/src/PublicAPI.Unshipped.txt b/src/SignalR/server/Core/src/PublicAPI.Unshipped.txt index 1feda0ec0c2a..860477539be5 100644 --- a/src/SignalR/server/Core/src/PublicAPI.Unshipped.txt +++ b/src/SignalR/server/Core/src/PublicAPI.Unshipped.txt @@ -1,9 +1,9 @@ #nullable enable Microsoft.AspNetCore.SignalR.HubOptions.DisableImplicitFromServicesParameters.get -> bool Microsoft.AspNetCore.SignalR.HubOptions.DisableImplicitFromServicesParameters.set -> void -Microsoft.AspNetCore.SignalR.IHubCallerClients.Single(string! connectionId) -> Microsoft.AspNetCore.SignalR.ISingleClientProxy! -Microsoft.AspNetCore.SignalR.IHubClients.Single(string! connectionId) -> Microsoft.AspNetCore.SignalR.ISingleClientProxy! -Microsoft.AspNetCore.SignalR.IHubClients.Single(string! connectionId) -> T +Microsoft.AspNetCore.SignalR.IHubCallerClients.Caller.get -> Microsoft.AspNetCore.SignalR.ISingleClientProxy! +Microsoft.AspNetCore.SignalR.IHubCallerClients.Client(string! connectionId) -> Microsoft.AspNetCore.SignalR.ISingleClientProxy! +Microsoft.AspNetCore.SignalR.IHubClients.Client(string! connectionId) -> Microsoft.AspNetCore.SignalR.ISingleClientProxy! Microsoft.AspNetCore.SignalR.ISingleClientProxy Microsoft.AspNetCore.SignalR.ISingleClientProxy.InvokeCoreAsync(string! method, object?[]! args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! override Microsoft.AspNetCore.SignalR.DefaultHubLifetimeManager.InvokeConnectionAsync(string! connectionId, string! methodName, object?[]! args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs index f72cd15f3b06..7ee5efc3b128 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs @@ -335,7 +335,7 @@ public async Task BlockingMethod() public async Task GetClientResult(int num) { - var sum = await Clients.Single(Context.ConnectionId).InvokeAsync("Sum", num); + var sum = await Clients.Caller.InvokeAsync("Sum", num); return sum; } } @@ -525,9 +525,8 @@ public Task SendToCaller(string message) return Clients.Caller.Send(message); } - public async Task GetClientResultThreeWays(int singleValue, int clientValue, int callerValue) => + public async Task GetClientResultTwoWays(int clientValue, int callerValue) => new ClientResults( - await Clients.Single(Context.ConnectionId).GetClientResult(singleValue), await Clients.Client(Context.ConnectionId).GetClientResult(clientValue), await Clients.Caller.GetClientResult(callerValue)); } @@ -540,7 +539,7 @@ public interface ITest Task GetClientResult(int value); } -public record ClientResults(int SingleResult, int ClientResult, int CallerResult); +public record ClientResults(int ClientResult, int CallerResult); public class OnConnectedThrowsHub : Hub { diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.ClientResult.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.ClientResult.cs index 8fdf84cc68e5..c348bce5083b 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.ClientResult.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.ClientResult.cs @@ -120,7 +120,7 @@ public async Task CanUseClientResultsWithIHubContext() await client.Connected.OrThrowIfOtherFails(connectionHandlerTask).DefaultTimeout(); var context = serviceProvider.GetRequiredService>(); - var resultTask = context.Clients.Single(client.Connection.ConnectionId).InvokeAsync("GetClientResult", 1); + var resultTask = context.Clients.Client(client.Connection.ConnectionId).InvokeAsync("GetClientResult", 1); var message = await client.ReadAsync().DefaultTimeout(); var invocation = Assert.IsType(message); @@ -154,28 +154,24 @@ public async Task CanUseClientResultsWithIHubContextT() var context = serviceProvider.GetRequiredService>(); - async Task AssertClientResult(Task resultTask) - { - var message = await client.ReadAsync().DefaultTimeout(); - var invocation = Assert.IsType(message); + var resultTask = context.Clients.Client(connectionId).GetClientResult(1); - Assert.Single(invocation.Arguments); - Assert.Equal(1L, invocation.Arguments[0]); - Assert.Equal("GetClientResult", invocation.Target); + var message = await client.ReadAsync().DefaultTimeout(); + var invocation = Assert.IsType(message); - await client.SendHubMessageAsync(CompletionMessage.WithResult(invocation.InvocationId, 2)).DefaultTimeout(); + Assert.Single(invocation.Arguments); + Assert.Equal(1L, invocation.Arguments[0]); + Assert.Equal("GetClientResult", invocation.Target); - var result = await resultTask.DefaultTimeout(); - Assert.Equal(2, result); - } + await client.SendHubMessageAsync(CompletionMessage.WithResult(invocation.InvocationId, 2)).DefaultTimeout(); - await AssertClientResult(context.Clients.Single(connectionId).GetClientResult(1)); - await AssertClientResult(context.Clients.Client(connectionId).GetClientResult(1)); + var result = await resultTask.DefaultTimeout(); + Assert.Equal(2, result); } } [Fact] - public async Task CanReturnClientResultToTypedHubThreeWays() + public async Task CanReturnClientResultToTypedHubTwoWays() { using (StartVerifiableLog()) { @@ -192,11 +188,11 @@ public async Task CanReturnClientResultToTypedHubThreeWays() var invocationId = await client.SendHubMessageAsync(new InvocationMessage( invocationId: "1", - nameof(HubT.GetClientResultThreeWays), - new object[] { 5, 6, 7 })).DefaultTimeout(); + nameof(HubT.GetClientResultTwoWays), + new object[] { 7, 3 })).DefaultTimeout(); - // Send back "value + 4" to all three invocations. - for (int i = 0; i < 3; i++) + // Send back "value + 4" to both invocations. + for (int i = 0; i < 2; i++) { // Hub asks client for a result, this is an invocation message with an ID. var invocationMessage = Assert.IsType(await client.ReadAsync().DefaultTimeout()); @@ -206,7 +202,7 @@ public async Task CanReturnClientResultToTypedHubThreeWays() } var completion = Assert.IsType(await client.ReadAsync().DefaultTimeout()); - Assert.Equal(new ClientResults(9, 10, 11), completion.Result); + Assert.Equal(new ClientResults(11, 7), completion.Result); } }