TypedSignalR.Client
2.1.0
See the version list below for details.
dotnet add package TypedSignalR.Client --version 2.1.0
NuGet\Install-Package TypedSignalR.Client -Version 2.1.0
<PackageReference Include="TypedSignalR.Client" Version="2.1.0" />
paket add TypedSignalR.Client --version 2.1.0
#r "nuget: TypedSignalR.Client, 2.1.0"
// Install TypedSignalR.Client as a Cake Addin
#addin nuget:?package=TypedSignalR.Client&version=2.1.0
// Install TypedSignalR.Client as a Cake Tool
#tool nuget:?package=TypedSignalR.Client&version=2.1.0
TypedSignalR.Client
C# Source Generator to create strongly typed SignalR Client.
Table of Contents
Install
NuGet: TypedSignalR.Client
dotnet add package Microsoft.AspNetCore.SignalR.Client
dotnet add package TypedSignalR.Client
Why TypedSignalR.Client?
The C # SignalR Client is untyped. To call a Hub (server-side) function, you must specify the function defined in Hub as a string.
connection.InvokeAsync("HubMethod")
You also have to manually determine the return type.
var ret = await connection.InvokeAsync<SomeType>("HubMethod")
Registering a client function called by the server also requires a string, and the argument types must be set manually.
Connection.On<string, DateTime>("ClientMethod", (str, dateTime) => {});
Therefore, if you change the code on the server-side, the modification on the client-side becomes very troublesome. The main cause is that it is not strongly typed.
This TypedSignalR.Client (Source Generator) aims to generate a strongly typed SignalR Client by sharing the server and client function definitions as an interface.
API
This Source Generator provides three extension methods and one interface.
static class Extensions
{
THub CreateHubProxy<THub>(this HubConnection source){...}
IDisposable Register<TReceiver>(this HubConnection source, TReceiver receiver){...}
(THub HubProxy, IDisposable Subscription) CreateHubProxyWith<THub, TReceiver>(this HubConnection source, TReceiver receiver){...}
}
// An interface for observing SigalR events.
interface IHubConnectionObserver
{
Task OnClosed(Exception e);
Task OnReconnected(string connectionId);
Task OnReconnecting(Exception e);
}
Use it as follows.
HubConnection connection = ...;
IHub hub = connection.CreateHubProxy<IHub>();
IDisposable subscription = connection.Register<IReceiver>(new Receiver());
Usage
Suppose you have the following interface defined:
public class UserDefine
{
public Guid RandomId { get; set; }
public DateTime Datetime { get; set; }
}
// The return type of the client-side method must be Task.
public interface IClientContract
{
// Of course, user defined type is OK.
Task ClientMethod1(string user, string message, UserDefine userDefine);
Task ClientMethod2();
}
// The return type of the method on the hub-side must be Task or Task <T>.
public interface IHubContract
{
Task<string> HubMethod1(string user, string message);
Task HubMethod2();
}
class Receiver1 : IClientContract
{
// impl
}
class Receiver2 : IClientContract, IHubConnectionObserver
{
// impl
}
Client
It's very easy to use.
HubConnection connection = ...;
var hub = connection.CreateHubProxy<IHubContract>();
var subscription1 = connection.Register<IClientContract>(new Receiver1());
// When an instance of a class that implements IHubConnectionObserver is registered (Receiver2 in this case),
// the method defined in IHubConnectionObserver is automatically registered regardless of the type argument.
var subscription2 = connection.Register<IClientContract>(new Receiver2());
// or
var (hub2, subscription3) = connection.CreateHubProxyWith<IHubContract, IClientContract>(new Receiver());
// Invoke hub methods
hub.HubMethod1("user", "message");
// Unregister the receiver
subscription.Dispose();
Cancellation
In pure SignalR, CancellationToken
is passed for each invoke.
On the other hand, in TypedSignalR.Client, CancellationToken
is passed only once when creating HubProxy.
The passed CancelationToken
will be used for each internal invoke.
var cts = new CancellationTokenSource();
// The following two are equivalent.
// pure SignalR
var ret= await connection.InvokeAsync<string>("HubMethod1", "user", "message", cts.Token);
await connection.InvokeAsync("HubMethod2", cts.Token);
// TypedSignalR.Client
var hubProxy = connection.CreateHubProxy<IHubContract>(cts.Token);
var ret = await hubProxy.HubMethod1("user", "message");
await hubProxy.HubMethod2();
Server
By the way, using these definitions, you can write as follows on the server side (ASP.NET Core).
TypedSignalR.Client
is not nessesary.
using Microsoft.AspNetCore.SignalR;
public class SomeHub : Hub<IClientContract>, IHubContract
{
public async Task<string> HubMethod1(string user, string message)
{
await this.Clients.All.ClientMethod1(user, message, new UserDefineClass());
return "OK!";
}
public async Task HubMethod2()
{
await this.Clients.Caller.ClientMethod2();
}
}
Recommendation
I recommend that these interfaces be shared between the client and server sides, for example, by project references.
server.csproj => shared.csproj <= client.csproj
Compile-time error support
This source generator has some restrictions, including those that come from the server side.
- Type argument of
CreateHubProxy/CreateHubProxyWith/Register
method must be an interface. - Only define methods in the interface used for
HubProxy/Receiver
.- Properties should not be defined.
- The return type of the method in the interface used for
HubProxy
must beTask
orTask<T>
. - The return type of the method in the interface used for
Receiver
must beTask
.
It is very difficult for humans to properly comply with these restrictions. Therefore, it is designed so that the compiler (Roslyn) looks for the part where the constraint is not observed at compile time and reports a detailed error. Therefore, no run-time error occurs.
Generated code
In this section, we will briefly explain what kind of code will be generated. The actual generated code can be seen in the Visual Studio dependencies.
The source generator checks the type argument of a method such as'CreateHubProxy/Register' and generates the following code based on it.
If we call the methods connection.CreateHubProxy<IHubContract>()
and connection.Register<IClientContract>(new Receiver())
, the following code will be generated (simplified here).
public static partial class Extensions
{
private class HubInvoker : IHubContract
{
private readonly HubConnection _connection;
public HubInvoker(HubConnection connection)
{
_connection = connection;
}
public Task<string> HubMethod1(string user, string message)
{
return _connection.InvokeCoreAsync<string>(nameof(HubMethod1), new object[] { user, message });
}
public Task HubMethod2()
{
return _connection.InvokeCoreAsync(nameof(HubMethod2), System.Array.Empty<object>());
}
}
private static CompositeDisposable BindIClientContract(HubConnection connection, IClientContract receiver)
{
var d1 = connection.On<string, string UserDefine>(nameof(receiver.ClientMethod1), receiver.ClientMethod1);
var d2 = connection.On(nameof(receiver.ClientMethod2), receiver.ClientMethod2);
var compositeDisposable = new CompositeDisposable();
compositeDisposable.Add(d1);
compositeDisposable.Add(d2);
return compositeDisposable;
}
static Extensions()
{
HubInvokerConstructorCache<IHubContract>.Construct = static connection => new HubInvoker(connection);
ReceiverBinderCache<IClientContract>.Bind = BindIClientContract;
}
}
The generated code is used through the API as follows.
public static partial class Extensions
{
// static type caching
private static class HubInvokerConstructorCache<T>
{
public static Func<HubConnection, T> Construct;
}
// static type caching
private static class ReceiverBinderCache<T>
{
public static Func<HubConnection, T, CompositeDisposable> Bind;
}
public static THub CreateHubProxy<THub>(this HubConnection connection)
{
return HubInvokerConstructorCache<THub>.Construct(connection);
}
public static IDisposable Register<TReceiver>(this HubConnection connection, TReceiver receiver)
{
if(typeof(TReceiver) == typeof(IHubConnectionObserver))
{
// special subscription
return new HubConnectionObserverSubscription(connection, receiver as IHubConnectionObserver);;
}
var compositeDisposable = ReceiverBinderCache<TReceiver>.Bind(connection, receiver);
if (receiver is IHubConnectionObserver hubConnectionObserver)
{
var subscription = new HubConnectionObserverSubscription(connection, hubConnectionObserver);
compositeDisposable.Add(subscription);
}
return compositeDisposable;
}
}
Demo
First, launch server. Then access it from your browser and open the console(F12).
git clone https://github.com/nenoNaninu/TypedSignalR.Client.git
cd sandbox
dotnet run --project SignalR.Server/SignalR.Server.csproj
Execute the console app in another shell.
cd sandbox
dotnet run --project SignalR.Client/SignalR.Client.csproj
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
3.3.0 | 456 | 2/7/2023 |
3.2.1 | 1,152 | 11/27/2022 |
3.1.1 | 3,365 | 8/14/2022 |
3.0.7 | 534 | 6/2/2022 |
3.0.6 | 388 | 5/8/2022 |
3.0.4 | 339 | 4/22/2022 |
3.0.3 | 304 | 4/15/2022 |
3.0.2 | 356 | 3/6/2022 |
3.0.1 | 287 | 2/21/2022 |
2.1.0 | 225 | 12/26/2021 |
2.0.1 | 1,950 | 6/16/2021 |
2.0.0 | 256 | 6/10/2021 |
1.1.0 | 226 | 5/13/2021 |
1.0.2 | 221 | 5/12/2021 |
1.0.1 | 203 | 5/11/2021 |
1.0.0 | 201 | 5/11/2021 |
0.0.1 | 338 | 5/11/2021 |