Skip to content

Commit d4fccce

Browse files
authored
[SignalR] Replace Single(connectionId) with Client(connectionId) and Caller (#42236)
1 parent 9ac541b commit d4fccce

File tree

12 files changed

+96
-73
lines changed

12 files changed

+96
-73
lines changed

src/Components/Server/test/Circuits/ComponentHubTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public async Task CannotInvokeOnLocationChangedBeforeInitialization()
8585
mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());
8686
}
8787

88-
private static (Mock<IClientProxy>, ComponentHub) InitializeComponentHub()
88+
private static (Mock<ISingleClientProxy>, ComponentHub) InitializeComponentHub()
8989
{
9090
var ephemeralDataProtectionProvider = new EphemeralDataProtectionProvider();
9191
var circuitIdFactory = new CircuitIdFactory(ephemeralDataProtectionProvider);
@@ -112,7 +112,7 @@ private static (Mock<IClientProxy>, ComponentHub) InitializeComponentHub()
112112
// Here we mock out elements of the Hub that are typically configured
113113
// by SignalR as clients connect to the hub.
114114
var mockCaller = new Mock<IHubCallerClients>();
115-
var mockClientProxy = new Mock<IClientProxy>();
115+
var mockClientProxy = new Mock<ISingleClientProxy>();
116116
mockCaller.Setup(x => x.Caller).Returns(mockClientProxy.Object);
117117
hub.Clients = mockCaller.Object;
118118
var mockContext = new Mock<HubCallerContext>();

src/SignalR/clients/ts/FunctionalTests/Startup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<
232232
{
233233
try
234234
{
235-
var result = await hubContext.Clients.Single(id).InvokeAsync<int>("Result");
235+
var result = await hubContext.Clients.Client(id).InvokeAsync<int>("Result");
236236
return result.ToString(CultureInfo.InvariantCulture);
237237
}
238238
catch (Exception ex)

src/SignalR/server/Core/src/IHubCallerClients.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.SignalR.Internal;
5+
46
namespace Microsoft.AspNetCore.SignalR;
57

68
/// <summary>
@@ -13,5 +15,11 @@ public interface IHubCallerClients : IHubCallerClients<IClientProxy>
1315
/// </summary>
1416
/// <param name="connectionId">The connection ID.</param>
1517
/// <returns>A client caller.</returns>
16-
new ISingleClientProxy Single(string connectionId) => throw new NotImplementedException();
18+
new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubCallerClients<IClientProxy>)this).Client(connectionId), "IHubCallerClients.Client(string connectionId)");
19+
20+
/// <summary>
21+
/// Gets a proxy that can be used to invoke methods on the calling client and receive results.
22+
/// </summary>
23+
/// <returns>A client caller.</returns>
24+
new ISingleClientProxy Caller => new NonInvokingSingleClientProxy(((IHubCallerClients<IClientProxy>)this).Caller, "IHubCallerClients.Caller");
1725
}

src/SignalR/server/Core/src/IHubClients.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.SignalR.Internal;
5+
46
namespace Microsoft.AspNetCore.SignalR;
57

68
/// <summary>
@@ -13,5 +15,5 @@ public interface IHubClients : IHubClients<IClientProxy>
1315
/// </summary>
1416
/// <param name="connectionId">The connection ID.</param>
1517
/// <returns>A client caller.</returns>
16-
new ISingleClientProxy Single(string connectionId) => throw new NotImplementedException();
18+
new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubClients<IClientProxy>)this).Client(connectionId), "IHubClients.Client(string connectionId)");
1719
}

src/SignalR/server/Core/src/IHubClients`T.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,6 @@ namespace Microsoft.AspNetCore.SignalR;
99
/// <typeparam name="T">The client invoker type.</typeparam>
1010
public interface IHubClients<T>
1111
{
12-
/// <summary>
13-
/// Gets a <typeparamref name="T" /> that can be used to invoke methods on a single client connected to the hub and receive results.
14-
/// </summary>
15-
/// <param name="connectionId">The connection ID.</param>
16-
/// <returns>A client caller.</returns>
17-
T Single(string connectionId) => Client(connectionId);
18-
1912
/// <summary>
2013
/// Gets a <typeparamref name="T" /> that can be used to invoke methods on all clients connected to the hub.
2114
/// </summary>

src/SignalR/server/Core/src/Internal/HubCallerClients.cs

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,19 @@ public HubCallerClients(IHubClients hubClients, string connectionId, bool parall
1818
_parallelEnabled = parallelEnabled;
1919
}
2020

21-
private sealed class NotParallelSingleClientProxy : ISingleClientProxy
21+
IClientProxy IHubCallerClients<IClientProxy>.Caller => Caller;
22+
public ISingleClientProxy Caller
2223
{
23-
private readonly ISingleClientProxy _proxy;
24-
25-
public NotParallelSingleClientProxy(ISingleClientProxy hubClients)
26-
{
27-
_proxy = hubClients;
28-
}
29-
30-
public Task<T> InvokeCoreAsync<T>(string method, object?[] args, CancellationToken cancellationToken = default)
24+
get
3125
{
32-
throw new InvalidOperationException("Client results inside a Hub method requires HubOptions.MaximumParallelInvocationsPerClient to be greater than 1.");
33-
}
34-
35-
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
36-
{
37-
return _proxy.SendCoreAsync(method, args, cancellationToken);
26+
if (!_parallelEnabled)
27+
{
28+
return new NotParallelSingleClientProxy(_hubClients.Client(_connectionId));
29+
}
30+
return _hubClients.Client(_connectionId);
3831
}
3932
}
4033

41-
public ISingleClientProxy Single(string connectionId)
42-
{
43-
if (!_parallelEnabled)
44-
{
45-
return new NotParallelSingleClientProxy(_hubClients.Single(connectionId));
46-
}
47-
return _hubClients.Single(connectionId);
48-
}
49-
50-
public IClientProxy Caller => _hubClients.Client(_connectionId);
51-
5234
public IClientProxy Others => _hubClients.AllExcept(_currentConnectionId);
5335

5436
public IClientProxy All => _hubClients.All;
@@ -58,8 +40,13 @@ public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds)
5840
return _hubClients.AllExcept(excludedConnectionIds);
5941
}
6042

61-
public IClientProxy Client(string connectionId)
43+
IClientProxy IHubClients<IClientProxy>.Client(string connectionId) => Client(connectionId);
44+
public ISingleClientProxy Client(string connectionId)
6245
{
46+
if (!_parallelEnabled)
47+
{
48+
return new NotParallelSingleClientProxy(_hubClients.Client(connectionId));
49+
}
6350
return _hubClients.Client(connectionId);
6451
}
6552

@@ -97,4 +84,24 @@ public IClientProxy Users(IReadOnlyList<string> userIds)
9784
{
9885
return _hubClients.Users(userIds);
9986
}
87+
88+
private sealed class NotParallelSingleClientProxy : ISingleClientProxy
89+
{
90+
private readonly ISingleClientProxy _proxy;
91+
92+
public NotParallelSingleClientProxy(ISingleClientProxy hubClients)
93+
{
94+
_proxy = hubClients;
95+
}
96+
97+
public Task<T> InvokeCoreAsync<T>(string method, object?[] args, CancellationToken cancellationToken = default)
98+
{
99+
throw new InvalidOperationException("Client results inside a Hub method requires HubOptions.MaximumParallelInvocationsPerClient to be greater than 1.");
100+
}
101+
102+
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
103+
{
104+
return _proxy.SendCoreAsync(method, args, cancellationToken);
105+
}
106+
}
100107
}

src/SignalR/server/Core/src/Internal/HubClients.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,15 @@ public HubClients(HubLifetimeManager<THub> lifetimeManager)
1313
All = new AllClientProxy<THub>(_lifetimeManager);
1414
}
1515

16-
public ISingleClientProxy Single(string connectionId)
17-
{
18-
return new SingleClientProxy<THub>(_lifetimeManager, connectionId);
19-
}
20-
2116
public IClientProxy All { get; }
2217

2318
public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds)
2419
{
2520
return new AllClientsExceptProxy<THub>(_lifetimeManager, excludedConnectionIds);
2621
}
2722

28-
public IClientProxy Client(string connectionId)
23+
IClientProxy IHubClients<IClientProxy>.Client(string connectionId) => Client(connectionId);
24+
public ISingleClientProxy Client(string connectionId)
2925
{
3026
return new SingleClientProxy<THub>(_lifetimeManager, connectionId);
3127
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.SignalR.Internal;
5+
6+
internal sealed class NonInvokingSingleClientProxy : ISingleClientProxy
7+
{
8+
private readonly IClientProxy _clientProxy;
9+
private readonly string _memberName;
10+
11+
public NonInvokingSingleClientProxy(IClientProxy clientProxy, string memberName)
12+
{
13+
_clientProxy = clientProxy;
14+
_memberName = memberName;
15+
}
16+
17+
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) =>
18+
_clientProxy.SendCoreAsync(method, args, cancellationToken);
19+
20+
public Task<T> InvokeCoreAsync<T>(string method, object?[] args, CancellationToken cancellationToken = default) =>
21+
throw new NotImplementedException($"The default implementation of {_memberName} does not support client return results.");
22+
}

src/SignalR/server/Core/src/Internal/TypedHubClients.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public TypedHubClients(IHubCallerClients dynamicContext)
1212
_hubClients = dynamicContext;
1313
}
1414

15-
public T Client(string connectionId) => TypedClientBuilder<T>.Build(_hubClients.Single(connectionId));
15+
public T Client(string connectionId) => TypedClientBuilder<T>.Build(_hubClients.Client(connectionId));
1616

1717
public T All => TypedClientBuilder<T>.Build(_hubClients.All);
1818

src/SignalR/server/Core/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#nullable enable
22
Microsoft.AspNetCore.SignalR.HubOptions.DisableImplicitFromServicesParameters.get -> bool
33
Microsoft.AspNetCore.SignalR.HubOptions.DisableImplicitFromServicesParameters.set -> void
4-
Microsoft.AspNetCore.SignalR.IHubCallerClients.Single(string! connectionId) -> Microsoft.AspNetCore.SignalR.ISingleClientProxy!
5-
Microsoft.AspNetCore.SignalR.IHubClients.Single(string! connectionId) -> Microsoft.AspNetCore.SignalR.ISingleClientProxy!
6-
Microsoft.AspNetCore.SignalR.IHubClients<T>.Single(string! connectionId) -> T
4+
Microsoft.AspNetCore.SignalR.IHubCallerClients.Caller.get -> Microsoft.AspNetCore.SignalR.ISingleClientProxy!
5+
Microsoft.AspNetCore.SignalR.IHubCallerClients.Client(string! connectionId) -> Microsoft.AspNetCore.SignalR.ISingleClientProxy!
6+
Microsoft.AspNetCore.SignalR.IHubClients.Client(string! connectionId) -> Microsoft.AspNetCore.SignalR.ISingleClientProxy!
77
Microsoft.AspNetCore.SignalR.ISingleClientProxy
88
Microsoft.AspNetCore.SignalR.ISingleClientProxy.InvokeCoreAsync<T>(string! method, object?[]! args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<T>!
99
override Microsoft.AspNetCore.SignalR.DefaultHubLifetimeManager<THub>.InvokeConnectionAsync<T>(string! connectionId, string! methodName, object?[]! args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<T>!

src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ public async Task BlockingMethod()
335335

336336
public async Task<int> GetClientResult(int num)
337337
{
338-
var sum = await Clients.Single(Context.ConnectionId).InvokeAsync<int>("Sum", num);
338+
var sum = await Clients.Caller.InvokeAsync<int>("Sum", num);
339339
return sum;
340340
}
341341
}
@@ -525,9 +525,8 @@ public Task SendToCaller(string message)
525525
return Clients.Caller.Send(message);
526526
}
527527

528-
public async Task<ClientResults> GetClientResultThreeWays(int singleValue, int clientValue, int callerValue) =>
528+
public async Task<ClientResults> GetClientResultTwoWays(int clientValue, int callerValue) =>
529529
new ClientResults(
530-
await Clients.Single(Context.ConnectionId).GetClientResult(singleValue),
531530
await Clients.Client(Context.ConnectionId).GetClientResult(clientValue),
532531
await Clients.Caller.GetClientResult(callerValue));
533532
}
@@ -540,7 +539,7 @@ public interface ITest
540539
Task<int> GetClientResult(int value);
541540
}
542541

543-
public record ClientResults(int SingleResult, int ClientResult, int CallerResult);
542+
public record ClientResults(int ClientResult, int CallerResult);
544543

545544
public class OnConnectedThrowsHub : Hub
546545
{

src/SignalR/server/SignalR/test/HubConnectionHandlerTests.ClientResult.cs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public async Task CanUseClientResultsWithIHubContext()
120120
await client.Connected.OrThrowIfOtherFails(connectionHandlerTask).DefaultTimeout();
121121

122122
var context = serviceProvider.GetRequiredService<IHubContext<MethodHub>>();
123-
var resultTask = context.Clients.Single(client.Connection.ConnectionId).InvokeAsync<int>("GetClientResult", 1);
123+
var resultTask = context.Clients.Client(client.Connection.ConnectionId).InvokeAsync<int>("GetClientResult", 1);
124124

125125
var message = await client.ReadAsync().DefaultTimeout();
126126
var invocation = Assert.IsType<InvocationMessage>(message);
@@ -154,28 +154,24 @@ public async Task CanUseClientResultsWithIHubContextT()
154154

155155
var context = serviceProvider.GetRequiredService<IHubContext<HubT, ITest>>();
156156

157-
async Task AssertClientResult(Task<int> resultTask)
158-
{
159-
var message = await client.ReadAsync().DefaultTimeout();
160-
var invocation = Assert.IsType<InvocationMessage>(message);
157+
var resultTask = context.Clients.Client(connectionId).GetClientResult(1);
161158

162-
Assert.Single(invocation.Arguments);
163-
Assert.Equal(1L, invocation.Arguments[0]);
164-
Assert.Equal("GetClientResult", invocation.Target);
159+
var message = await client.ReadAsync().DefaultTimeout();
160+
var invocation = Assert.IsType<InvocationMessage>(message);
165161

166-
await client.SendHubMessageAsync(CompletionMessage.WithResult(invocation.InvocationId, 2)).DefaultTimeout();
162+
Assert.Single(invocation.Arguments);
163+
Assert.Equal(1L, invocation.Arguments[0]);
164+
Assert.Equal("GetClientResult", invocation.Target);
167165

168-
var result = await resultTask.DefaultTimeout();
169-
Assert.Equal(2, result);
170-
}
166+
await client.SendHubMessageAsync(CompletionMessage.WithResult(invocation.InvocationId, 2)).DefaultTimeout();
171167

172-
await AssertClientResult(context.Clients.Single(connectionId).GetClientResult(1));
173-
await AssertClientResult(context.Clients.Client(connectionId).GetClientResult(1));
168+
var result = await resultTask.DefaultTimeout();
169+
Assert.Equal(2, result);
174170
}
175171
}
176172

177173
[Fact]
178-
public async Task CanReturnClientResultToTypedHubThreeWays()
174+
public async Task CanReturnClientResultToTypedHubTwoWays()
179175
{
180176
using (StartVerifiableLog())
181177
{
@@ -192,11 +188,11 @@ public async Task CanReturnClientResultToTypedHubThreeWays()
192188

193189
var invocationId = await client.SendHubMessageAsync(new InvocationMessage(
194190
invocationId: "1",
195-
nameof(HubT.GetClientResultThreeWays),
196-
new object[] { 5, 6, 7 })).DefaultTimeout();
191+
nameof(HubT.GetClientResultTwoWays),
192+
new object[] { 7, 3 })).DefaultTimeout();
197193

198-
// Send back "value + 4" to all three invocations.
199-
for (int i = 0; i < 3; i++)
194+
// Send back "value + 4" to both invocations.
195+
for (int i = 0; i < 2; i++)
200196
{
201197
// Hub asks client for a result, this is an invocation message with an ID.
202198
var invocationMessage = Assert.IsType<InvocationMessage>(await client.ReadAsync().DefaultTimeout());
@@ -206,7 +202,7 @@ public async Task CanReturnClientResultToTypedHubThreeWays()
206202
}
207203

208204
var completion = Assert.IsType<CompletionMessage>(await client.ReadAsync().DefaultTimeout());
209-
Assert.Equal(new ClientResults(9, 10, 11), completion.Result);
205+
Assert.Equal(new ClientResults(11, 7), completion.Result);
210206
}
211207
}
212208

0 commit comments

Comments
 (0)