The Wayback Machine - https://web.archive.org/web/20210510000021/https://github.com/dotnet/aspnetcore/issues/32534
Skip to content
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

[Design proposal] Strongly-typed SignalR hub proxies and callback handlers #32534

Open
mehmetakbulut opened this issue May 9, 2021 · 0 comments
Open

Comments

@mehmetakbulut
Copy link

@mehmetakbulut mehmetakbulut commented May 9, 2021

Summary

This proposal aims to provide end-to-end strong typing for all interactions between SignalR clients and SignalR hubs.

Motivation and goals

  • Currently, SignalR clients can neither make strongly-typed calls to a SignalR hub nor register handlers for server-to-client calls in a strongly-typed manner. As such, developers are forced to write magic strings as well as guess argument sequence and return type.
  • Community has asked for this over the years and people had to implement their own workarounds including mine.
  • With end-to-end strong typing:
    • Many run-time errors will be caught as build-time errors.
    • Refactoring solutions that has both client and server projects will be easier since both ends can now share an actual interface.

In scope

  1. Strongly-typed remote procedure calls from a SignalR client to a SignalR hub
  2. Strongly-typed streaming calls from a SignalR client to a SignalR hub
    1. Support for both single direction streaming in either direction and bi-directional streaming
    2. Support for cancellation token in streaming calls.
    3. Support for both ChannelReader<> and IAsyncEnumerable<> as the underlying stream
  3. Strongly-typed handlers for calls from a SignalR hub to a SignalR client

Out of scope

  1. Non-C# usage. For the time being, we should focus on design and implementation of the ideal solution. Later we can think about how this design can be implemented in languages other than C# based on collaboration with the community.

Risks / unknowns

  1. Transformative features (e.g. HubMethodName) can break strongly-typed clients depending on the particular implementation.
    1. Should we allow interface definitions to be annotated with attributes so the consumers can account for a subset of these issues?
  2. Different implementations have different performance.
    1. Is there a level of performance that we would want to guarantee across all target platforms?
  3. Some designs may not be possible to implement for some platforms.
    1. Which platforms should we target?
    2. Would we be OK with different implementations for different platforms?
  4. Versioning may be hard.

Examples

Client to Server Calls

Let's say there is an interface IMyHub as defined below which is implemented by an ASP.NET Core application as MyHub : SignalR.Hub, IMyHub.

    public interface IMyHub
    {
        Task Do();

        Task<int> Get();

        Task Set(int a);
        
        Task<int> GetAndSet(int a);

        Task<ChannelReader<int>> StreamToClientViaChannel();

        Task<ChannelReader<int>> StreamToClientViaChannelWithToken(CancellationToken cancellationToken);

        Task StreamFromClientViaChannel(ChannelReader<int> reader);

        IAsyncEnumerable<int> StreamToClientViaEnumerableWithToken([EnumeratorCancellation] CancellationToken cancellationToken);

        Task StreamFromClientViaEnumerable(IAsyncEnumerable<int> reader);
    }

A developer currently needs to consume it as below.

await hubConnection.InvokeAsync("Do");
var ret = (int) await hubConnection.SendAsync("GetAndSet", 100);

Instead, developer could be making strongly-typed calls.

await myHub.Do();
var ret = await myHub.GetAndSet(100);

We can either have such a proxy be acquired from a hub connection or a builder.

var myHub = myHubConnection.AsHubProxy<IMyHub>();
// vs
var myHub = new HubProxyBuilder<IMyHub>()
    .WithConnection(myHubConnection)
    .ConfigureAnythingElse()
    .Build();

Acquisiton from a hub connection is simpler while builder provides more room for extension in future.

Server to Client Call Handlers

One can similarly define an interface IMyClient as below which can then be used in Hub<IMyClient> on server-end and implemented by any consumer to provide callbacks.

    public interface IMyClient
    {
        void Callback1();
        void Callback2(string arg);
    }

A developer currently needs to provide such callbacks as below.

await hubConnection.On("Callback1", (req) => { someActivity1(); });
await hubConnection.On<string>("Callback2", (arg) => { someActivity2(arg); });

Instead, developer could be registering callback in a strongly-typed manner.

public class MyClient : IMyClient { .. }
var myClient = new MyClient();
await hubConnection.RegisterCallbacks<IMyClient>(myClient);

Multiple callback providers can be registered against the same hub connection so different callbacks can be provided by different classes. However this does mean overlap is possible. We'd want to decide how to handle this and whether to impose restrictions.

Detailed design

To be decided.

Some alternatives for client-to-server call implementation are:
Source-generated proxies utilizes C# 9 / .NET 5 source generator feature.

Pros:

  • Supported on JIT and AOT platforms

Cons:

  • Consumer must be a C# 9 / .NET 5 project (though indirect consumption may be possible)

Dynamic proxies utilizes Reflection.Emit to dynamically generate proxy.

Pros:

  • Consumer can be a .NET Standard 2.0 project

Cons:

  • Not supported on AOT platforms
  • Third-party dependency is needed if we don't want to reimplement a lot of proxy generation code or introduce significant constraints.

Expressive proxies utilizes expressions to

Pros:

  • Mostly universal platform support

Cons:

  • Slowest
  • More verbose usage (e.g. await hubConnection.AsExpressive<IMyHub>().InvokeAsync(hub => hub.Do()))

Server-to-client calls can be registered with just reflection (no Reflection.Emit) which is simple enough and would work on practically any platform. Other alternatives are possible such as source generation as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
1 participant