dotnet / dotnet/aspnetcore

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

Open
#32,534 26 comments 39 reactions 1 assignee Claimed by @BrennanConroy View on GitHub
area-signalr design-proposal
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

## 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](https://github.com/dotnet/aspnetcore/issues/15198) [this over the years](https://github.com/dotnet/aspnetcore/issues/5278) and [people had to](https://github.com/ocbaker/SignalRNetClientProxyMap) [implement their](https://github.com/JasonEdb/SignalR.TypedClient) [own workarounds](https://github.com/MarkNickeson/SignalrCoreClientSideProxies) [including mine](https://github.com/mehmetakbulut/SignalR.Strong).
- 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`.

```csharp
public interface IMyHub
{
Task Do();

Task Get();

Task Set(int a);

Task GetAndSet(int a);

Task> StreamToClientViaChannel();

Task> StreamToClientViaChannelWithToken(CancellationToken cancellationToken);

Task StreamFromClientViaChannel(ChannelReader reader);

IAsyncEnumerable StreamToClientViaEnumerableWithToken([EnumeratorCancellation] CancellationToken cancellationToken);

Task StreamFromClientViaEnumerable(IAsyncEnumerable reader);
}
```

A developer currently needs to consume it as below.
```csharp
await hubConnection.InvokeAsync("Do");
var ret = (int) await hubConnection.SendAsync("GetAndSet", 100);
```

Instead, developer could be making strongly-typed calls.
```csharp
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.
```csharp
var myHub = myHubConnection.AsHubProxy();
// vs
var myHub = new HubProxyBuilder()
.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` on server-end and implemented by any consumer to provide callbacks.
```csharp
public interface IMyClient
{
void Callback1();
void Callback2(string arg);
}
```

A developer currently needs to provide such callbacks as below.
```csharp
await hubConnection.On("Callback1", (req) => { someActivity1(); });
await hubConnection.On("Callback2", (arg) => { someActivity2(arg); });
```

Instead, developer could be registering callback in a strongly-typed manner.
```csharp
public class MyClient : IMyClient { .. }
var myClient = new MyClient();
await hubConnection.RegisterCallbacks(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](http://www.castleproject.org/projects/dynamicproxy/) is needed if we don't want to reimplement a lot of proxy generation code or introduce significant constraints.

**Expressive proxies** utilizes expressions to emulate a strongly-typed user experience.

Pros:
- Mostly universal platform support

Cons:
- Slowest
- More verbose usage (e.g. `await hubConnection.AsExpressive().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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.