Skip to content

Commit a6b1101

Browse files
authored
Allow returns from all single client proxies (#41939)
1 parent 11ac90e commit a6b1101

File tree

8 files changed

+75
-45
lines changed

8 files changed

+75
-45
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public interface IHubClients<T>
1414
/// </summary>
1515
/// <param name="connectionId">The connection ID.</param>
1616
/// <returns>A client caller.</returns>
17-
T Single(string connectionId) => throw new NotImplementedException();
17+
T Single(string connectionId) => Client(connectionId);
1818

1919
/// <summary>
2020
/// Gets a <typeparamref name="T" /> that can be used to invoke methods on all clients connected to the hub.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public HubClients(HubLifetimeManager<THub> lifetimeManager)
1515

1616
public ISingleClientProxy Single(string connectionId)
1717
{
18-
return new SingleClientProxyWithInvoke<THub>(_lifetimeManager, connectionId);
18+
return new SingleClientProxy<THub>(_lifetimeManager, connectionId);
1919
}
2020

2121
public IClientProxy All { get; }

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ public HubClients(HubLifetimeManager<THub> lifetimeManager)
1515

1616
public T All { get; }
1717

18-
public T Single(string connectionId)
19-
{
20-
return TypedClientBuilder<T>.Build(new SingleClientProxyWithInvoke<THub>(_lifetimeManager, connectionId));
21-
}
22-
2318
public T AllExcept(IReadOnlyList<string> excludedConnectionIds)
2419
{
2520
return TypedClientBuilder<T>.Build(new AllClientsExceptProxy<THub>(_lifetimeManager, excludedConnectionIds));

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

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -122,23 +122,6 @@ public Task SendCoreAsync(string method, object?[] args, CancellationToken cance
122122
}
123123
}
124124

125-
internal sealed class SingleClientProxy<THub> : IClientProxy where THub : Hub
126-
{
127-
private readonly string _connectionId;
128-
private readonly HubLifetimeManager<THub> _lifetimeManager;
129-
130-
public SingleClientProxy(HubLifetimeManager<THub> lifetimeManager, string connectionId)
131-
{
132-
_lifetimeManager = lifetimeManager;
133-
_connectionId = connectionId;
134-
}
135-
136-
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
137-
{
138-
return _lifetimeManager.SendConnectionAsync(_connectionId, method, args, cancellationToken);
139-
}
140-
}
141-
142125
internal sealed class MultipleClientProxy<THub> : IClientProxy where THub : Hub
143126
{
144127
private readonly HubLifetimeManager<THub> _lifetimeManager;
@@ -156,12 +139,12 @@ public Task SendCoreAsync(string method, object?[] args, CancellationToken cance
156139
}
157140
}
158141

159-
internal sealed class SingleClientProxyWithInvoke<THub> : ISingleClientProxy where THub : Hub
142+
internal sealed class SingleClientProxy<THub> : ISingleClientProxy where THub : Hub
160143
{
161144
private readonly string _connectionId;
162145
private readonly HubLifetimeManager<THub> _lifetimeManager;
163146

164-
public SingleClientProxyWithInvoke(HubLifetimeManager<THub> lifetimeManager, string connectionId)
147+
public SingleClientProxy(HubLifetimeManager<THub> lifetimeManager, string connectionId)
165148
{
166149
_lifetimeManager = lifetimeManager;
167150
_connectionId = connectionId;

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

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

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

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

@@ -22,11 +22,6 @@ public TypedHubClients(IHubCallerClients dynamicContext)
2222

2323
public T AllExcept(IReadOnlyList<string> excludedConnectionIds) => TypedClientBuilder<T>.Build(_hubClients.AllExcept(excludedConnectionIds));
2424

25-
public T Client(string connectionId)
26-
{
27-
return TypedClientBuilder<T>.Build(_hubClients.Client(connectionId));
28-
}
29-
3025
public T Group(string groupName)
3126
{
3227
return TypedClientBuilder<T>.Build(_hubClients.Group(groupName));

src/SignalR/server/SignalR/test/ClientProxyTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ public async Task SingleClientProxyWithInvoke_ThrowsNotSupported()
212212
{
213213
var hubLifetimeManager = new EmptyHubLifetimeManager<FakeHub>();
214214

215-
var proxy = new SingleClientProxyWithInvoke<FakeHub>(hubLifetimeManager, "");
215+
var proxy = new SingleClientProxy<FakeHub>(hubLifetimeManager, "");
216216
var ex = await Assert.ThrowsAsync<NotImplementedException>(async () => await proxy.InvokeAsync<int>("method")).DefaultTimeout();
217217
Assert.Equal("EmptyHubLifetimeManager`1 does not support client return values.", ex.Message);
218218
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ public Task SendToCaller(string message)
440440
}
441441
}
442442

443-
public class HubT : Hub<Test>
443+
public class HubT : Hub<ITest>
444444
{
445445
public override Task OnConnectedAsync()
446446
{
@@ -524,16 +524,24 @@ public Task SendToCaller(string message)
524524
{
525525
return Clients.Caller.Send(message);
526526
}
527+
528+
public async Task<ClientResults> GetClientResultThreeWays(int singleValue, int clientValue, int callerValue) =>
529+
new ClientResults(
530+
await Clients.Single(Context.ConnectionId).GetClientResult(singleValue),
531+
await Clients.Client(Context.ConnectionId).GetClientResult(clientValue),
532+
await Clients.Caller.GetClientResult(callerValue));
527533
}
528534

529-
public interface Test
535+
public interface ITest
530536
{
531537
Task Send(string message);
532538
Task Broadcast(string message);
533539

534540
Task<int> GetClientResult(int value);
535541
}
536542

543+
public record ClientResults(int SingleResult, int ClientResult, int CallerResult);
544+
537545
public class OnConnectedThrowsHub : Hub
538546
{
539547
public override Task OnConnectedAsync()

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

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,26 +145,75 @@ public async Task CanUseClientResultsWithIHubContextT()
145145
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<HubT>>();
146146

147147
using var client = new TestClient();
148+
var connectionId = client.Connection.ConnectionId;
148149

149150
var connectionHandlerTask = await client.ConnectAsync(connectionHandler);
150151

151152
// Wait for a connection, or for the endpoint to fail.
152153
await client.Connected.OrThrowIfOtherFails(connectionHandlerTask).DefaultTimeout();
153154

154-
var context = serviceProvider.GetRequiredService<IHubContext<HubT, Test>>();
155-
var resultTask = context.Clients.Single(client.Connection.ConnectionId).GetClientResult(1);
155+
var context = serviceProvider.GetRequiredService<IHubContext<HubT, ITest>>();
156156

157-
var message = await client.ReadAsync().DefaultTimeout();
158-
var invocation = Assert.IsType<InvocationMessage>(message);
157+
async Task AssertClientResult(Task<int> resultTask)
158+
{
159+
var message = await client.ReadAsync().DefaultTimeout();
160+
var invocation = Assert.IsType<InvocationMessage>(message);
159161

160-
Assert.Single(invocation.Arguments);
161-
Assert.Equal(1L, invocation.Arguments[0]);
162-
Assert.Equal("GetClientResult", invocation.Target);
162+
Assert.Single(invocation.Arguments);
163+
Assert.Equal(1L, invocation.Arguments[0]);
164+
Assert.Equal("GetClientResult", invocation.Target);
163165

164-
await client.SendHubMessageAsync(CompletionMessage.WithResult(invocation.InvocationId, 2)).DefaultTimeout();
166+
await client.SendHubMessageAsync(CompletionMessage.WithResult(invocation.InvocationId, 2)).DefaultTimeout();
165167

166-
var result = await resultTask.DefaultTimeout();
167-
Assert.Equal(2, result);
168+
var result = await resultTask.DefaultTimeout();
169+
Assert.Equal(2, result);
170+
}
171+
172+
await AssertClientResult(context.Clients.Single(connectionId).GetClientResult(1));
173+
await AssertClientResult(context.Clients.Client(connectionId).GetClientResult(1));
168174
}
169175
}
176+
177+
[Fact]
178+
public async Task CanReturnClientResultToTypedHubThreeWays()
179+
{
180+
using (StartVerifiableLog())
181+
{
182+
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(builder =>
183+
{
184+
// Waiting for a client result blocks the hub dispatcher pipeline, need to allow multiple invocations
185+
builder.AddSignalR(o => o.MaximumParallelInvocationsPerClient = 2);
186+
}, LoggerFactory);
187+
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<HubT>>();
188+
189+
using var client = new TestClient(invocationBinder: new GetClientResultThreeWaysInvocationBinder());
190+
191+
var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout();
192+
193+
var invocationId = await client.SendHubMessageAsync(new InvocationMessage(
194+
invocationId: "1",
195+
nameof(HubT.GetClientResultThreeWays),
196+
new object[] { 5, 6, 7 })).DefaultTimeout();
197+
198+
// Send back "value + 4" to all three invocations.
199+
for (int i = 0; i < 3; i++)
200+
{
201+
// Hub asks client for a result, this is an invocation message with an ID.
202+
var invocationMessage = Assert.IsType<InvocationMessage>(await client.ReadAsync().DefaultTimeout());
203+
Assert.NotNull(invocationMessage.InvocationId);
204+
var res = 4 + (int)invocationMessage.Arguments[0];
205+
await client.SendHubMessageAsync(CompletionMessage.WithResult(invocationMessage.InvocationId, res)).DefaultTimeout();
206+
}
207+
208+
var completion = Assert.IsType<CompletionMessage>(await client.ReadAsync().DefaultTimeout());
209+
Assert.Equal(new ClientResults(9, 10, 11), completion.Result);
210+
}
211+
}
212+
213+
private class GetClientResultThreeWaysInvocationBinder : IInvocationBinder
214+
{
215+
public IReadOnlyList<Type> GetParameterTypes(string methodName) => new[] { typeof(int) };
216+
public Type GetReturnType(string invocationId) => typeof(ClientResults);
217+
public Type GetStreamItemType(string streamId) => throw new NotImplementedException();
218+
}
170219
}

0 commit comments

Comments
 (0)