Skip to content

Replace Single(connectionId) with Client(connectionId) and Caller #41994

Closed
@halter73

Description

@halter73

Background and Motivation

When approving API for #5280 we considered replacing Single with new Caller and new Client members but we were worried it would be source breaking because:

  • The suggestion of using new Caller and Client methods ISingleClientProxy would be source breaking with existing IHubClients and IHubCallerClients implementations, decorators etc... if someone replace the IHubContext<> in DI, so we'll stick with Single

However further investigation shows that we can add new Caller and Client members without breaking existing IHubClients and IHubCallerClients implementations unless they're being used for client results which never worked before .NET 7 making the change both non-binary and non-source breaking[1].

Proposed API

namespace Microsoft.AspNetCore.SignalR;

public interface IHubClients<T>
{
-   T Single(string connectionId) => throw new NotImplementedException();
}


public interface IHubClients : IHubClients<IClientProxy>
{
-   new ISingleClientProxy Single(string connectionId) => throw new NotImplementedException();
+   new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubClients<IClientProxy>)this).//...
}

public interface IHubCallerClients : IHubCallerClients<IClientProxy>
{
-   new ISingleClientProxy Single(string connectionId) => throw new NotImplementedException();
+   new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubCallerClients<IClientProxy>)this).//...
+   new ISingleClientProxy Caller => new NonInvokingSingleClientProxy(((IHubCallerClients<IClientProxy>)this).//...
}

Usage Examples

public class MyHub : Hub
{
    public async Task BroadcastCallerResult()
    {
        var num = await Clients.Caller.InvokeAsync<int>("GetNum");
        await Clients.Others.SendAsync("BroadcastNum", num);
    }
}

Alternative Designs

  1. Keep the existing Single(connectionId) method and add new Caller and new Client.
  2. Leave it as is with just Single(connectionId) being the only way to get client return results.

Risks

  1. This is within reason. Even before, if someone had a custom interface that implemented IHubClients and added a void Single(string whatever), that would be source breaking. I don't think anyone has done this though. I also don't think anyone is doing anything like the following which would still technically be source breaking:
public class MyHub : Hub
{
    public void SourceBreakingMethod()
    {
        var selectedProxy = Clients.Caller;
        selectedProxy = Clients.All; // <--- Cannot implicitly convert type 'Microsoft.AspNetCore.SignalR.IClientProxy' to 'Microsoft.AspNetCore.SignalR.ISingleClientProxy'.
    }
}

But that involves using the returned type of Caller or Client(connectionId) to infer a non-covariant type. The above example is the easiest way I can think of doing this, but I don't think anyone does this in practice.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-signalrIncludes: SignalR clients and serversbreaking-changeThis issue / pr will introduce a breaking change, when resolved / merged.

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions