Skip to content

[SignalR] Replace Single(connectionId) with Client(connectionId) and Caller #42236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Components/Server/test/Circuits/ComponentHubTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task CannotInvokeOnLocationChangedBeforeInitialization()
mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());
}

private static (Mock<IClientProxy>, ComponentHub) InitializeComponentHub()
private static (Mock<ISingleClientProxy>, ComponentHub) InitializeComponentHub()
{
var ephemeralDataProtectionProvider = new EphemeralDataProtectionProvider();
var circuitIdFactory = new CircuitIdFactory(ephemeralDataProtectionProvider);
Expand All @@ -112,7 +112,7 @@ private static (Mock<IClientProxy>, 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<IHubCallerClients>();
var mockClientProxy = new Mock<IClientProxy>();
var mockClientProxy = new Mock<ISingleClientProxy>();
mockCaller.Setup(x => x.Caller).Returns(mockClientProxy.Object);
hub.Clients = mockCaller.Object;
var mockContext = new Mock<HubCallerContext>();
Expand Down
2 changes: 1 addition & 1 deletion src/SignalR/clients/ts/FunctionalTests/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<
{
try
{
var result = await hubContext.Clients.Single(id).InvokeAsync<int>("Result");
var result = await hubContext.Clients.Client(id).InvokeAsync<int>("Result");
return result.ToString(CultureInfo.InvariantCulture);
}
catch (Exception ex)
Expand Down
10 changes: 9 additions & 1 deletion src/SignalR/server/Core/src/IHubCallerClients.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
Expand All @@ -13,5 +15,11 @@ public interface IHubCallerClients : IHubCallerClients<IClientProxy>
/// </summary>
/// <param name="connectionId">The connection ID.</param>
/// <returns>A client caller.</returns>
new ISingleClientProxy Single(string connectionId) => throw new NotImplementedException();
new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubCallerClients<IClientProxy>)this).Client(connectionId), "IHubCallerClients.Client(string connectionId)");

/// <summary>
/// Gets a proxy that can be used to invoke methods on the calling client and receive results.
/// </summary>
/// <returns>A client caller.</returns>
new ISingleClientProxy Caller => new NonInvokingSingleClientProxy(((IHubCallerClients<IClientProxy>)this).Caller, "IHubCallerClients.Caller");
}
4 changes: 3 additions & 1 deletion src/SignalR/server/Core/src/IHubClients.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
Expand All @@ -13,5 +15,5 @@ public interface IHubClients : IHubClients<IClientProxy>
/// </summary>
/// <param name="connectionId">The connection ID.</param>
/// <returns>A client caller.</returns>
new ISingleClientProxy Single(string connectionId) => throw new NotImplementedException();
new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubClients<IClientProxy>)this).Client(connectionId), "IHubClients.Client(string connectionId)");
}
7 changes: 0 additions & 7 deletions src/SignalR/server/Core/src/IHubClients`T.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@ namespace Microsoft.AspNetCore.SignalR;
/// <typeparam name="T">The client invoker type.</typeparam>
public interface IHubClients<T>
{
/// <summary>
/// Gets a <typeparamref name="T" /> that can be used to invoke methods on a single client connected to the hub and receive results.
/// </summary>
/// <param name="connectionId">The connection ID.</param>
/// <returns>A client caller.</returns>
T Single(string connectionId) => Client(connectionId);

/// <summary>
/// Gets a <typeparamref name="T" /> that can be used to invoke methods on all clients connected to the hub.
/// </summary>
Expand Down
61 changes: 34 additions & 27 deletions src/SignalR/server/Core/src/Internal/HubCallerClients.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,19 @@ public HubCallerClients(IHubClients hubClients, string connectionId, bool parall
_parallelEnabled = parallelEnabled;
}

private sealed class NotParallelSingleClientProxy : ISingleClientProxy
IClientProxy IHubCallerClients<IClientProxy>.Caller => Caller;
public ISingleClientProxy Caller
{
private readonly ISingleClientProxy _proxy;

public NotParallelSingleClientProxy(ISingleClientProxy hubClients)
{
_proxy = hubClients;
}

public Task<T> InvokeCoreAsync<T>(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;
Expand All @@ -58,8 +40,13 @@ public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds)
return _hubClients.AllExcept(excludedConnectionIds);
}

public IClientProxy Client(string connectionId)
IClientProxy IHubClients<IClientProxy>.Client(string connectionId) => Client(connectionId);
public ISingleClientProxy Client(string connectionId)
{
if (!_parallelEnabled)
{
return new NotParallelSingleClientProxy(_hubClients.Client(connectionId));
}
return _hubClients.Client(connectionId);
}

Expand Down Expand Up @@ -97,4 +84,24 @@ public IClientProxy Users(IReadOnlyList<string> userIds)
{
return _hubClients.Users(userIds);
}

private sealed class NotParallelSingleClientProxy : ISingleClientProxy
{
private readonly ISingleClientProxy _proxy;

public NotParallelSingleClientProxy(ISingleClientProxy hubClients)
{
_proxy = hubClients;
}

public Task<T> InvokeCoreAsync<T>(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);
}
}
}
8 changes: 2 additions & 6 deletions src/SignalR/server/Core/src/Internal/HubClients.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,15 @@ public HubClients(HubLifetimeManager<THub> lifetimeManager)
All = new AllClientProxy<THub>(_lifetimeManager);
}

public ISingleClientProxy Single(string connectionId)
{
return new SingleClientProxy<THub>(_lifetimeManager, connectionId);
}

public IClientProxy All { get; }

public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds)
{
return new AllClientsExceptProxy<THub>(_lifetimeManager, excludedConnectionIds);
}

public IClientProxy Client(string connectionId)
IClientProxy IHubClients<IClientProxy>.Client(string connectionId) => Client(connectionId);
public ISingleClientProxy Client(string connectionId)
{
return new SingleClientProxy<THub>(_lifetimeManager, connectionId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T> InvokeCoreAsync<T>(string method, object?[] args, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"The default implementation of {_memberName} does not support client return results.");
}
2 changes: 1 addition & 1 deletion src/SignalR/server/Core/src/Internal/TypedHubClients.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public TypedHubClients(IHubCallerClients dynamicContext)
_hubClients = dynamicContext;
}

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

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

Expand Down
6 changes: 3 additions & 3 deletions src/SignalR/server/Core/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -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<T>.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<T>(string! method, object?[]! args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<T>!
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>!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ public async Task BlockingMethod()

public async Task<int> GetClientResult(int num)
{
var sum = await Clients.Single(Context.ConnectionId).InvokeAsync<int>("Sum", num);
var sum = await Clients.Caller.InvokeAsync<int>("Sum", num);
return sum;
}
}
Expand Down Expand Up @@ -525,9 +525,8 @@ public Task SendToCaller(string message)
return Clients.Caller.Send(message);
}

public async Task<ClientResults> GetClientResultThreeWays(int singleValue, int clientValue, int callerValue) =>
public async Task<ClientResults> 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));
}
Expand All @@ -540,7 +539,7 @@ public interface ITest
Task<int> GetClientResult(int value);
}

public record ClientResults(int SingleResult, int ClientResult, int CallerResult);
public record ClientResults(int ClientResult, int CallerResult);

public class OnConnectedThrowsHub : Hub
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public async Task CanUseClientResultsWithIHubContext()
await client.Connected.OrThrowIfOtherFails(connectionHandlerTask).DefaultTimeout();

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

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

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

async Task AssertClientResult(Task<int> resultTask)
{
var message = await client.ReadAsync().DefaultTimeout();
var invocation = Assert.IsType<InvocationMessage>(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<InvocationMessage>(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())
{
Expand All @@ -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<InvocationMessage>(await client.ReadAsync().DefaultTimeout());
Expand All @@ -206,7 +202,7 @@ public async Task CanReturnClientResultToTypedHubThreeWays()
}

var completion = Assert.IsType<CompletionMessage>(await client.ReadAsync().DefaultTimeout());
Assert.Equal(new ClientResults(9, 10, 11), completion.Result);
Assert.Equal(new ClientResults(11, 7), completion.Result);
}
}

Expand Down