Giter Club home page Giter Club logo

typedsignalr.client's Introduction

TypedSignalR.Client

NuGet build-and-test

C# Source Generator to create strongly typed SignalR clients.

Table of Contents

Install

NuGet: TypedSignalR.Client

dotnet add package Microsoft.AspNetCore.SignalR.Client
dotnet add package TypedSignalR.Client

Why TypedSignalR.Client?

The ASP.NET Core SignalR C# client is not strongly typed. To call a Hub (server-side) method, we must specify the method defined in Hub using a string. We also have to determine the return type manually. Moreover, registering client methods called from a server also requires specifying the method name as a string, and we must set parameter types manually.

// C# SignalR Client
// without TypedSignalR.Client

// Specify a hub method to invoke using string.
await connection.InvokeAsync("HubMethod1");

// Manually determine a return type.
// Parameters are cast to object type.
var guid = await connection.InvokeAsync<Guid>("HubMethod2", "message", 99);

// Registering a client method requires a string, and parameter types must be set manually.
var subscription = connection.On<string, DateTime>("ClientMethod", (message, dateTime) => {});

These are very painful and cause bugs easily. Moreover, if we change the code on the server-side, the modification on the client-side becomes very troublesome. The leading cause of the problems is that they are not strongly typed.

TypedSignalR.Client aims to generate strongly typed SignalR clients using interfaces in which the server and client methods are defined. Defining interfaces is helpful not only for the client-side but also for the server-side. See Usage section for details.

// C# SignalR Client
// with TypedSignalR.Client

// First, create a hub proxy.
IHub hubProxy = connection.CreateHubProxy<IHub>();

// Invoke a hub method through hub proxy.
// We no longer need to specify the method using a string.
await hubProxy.HubMethod1();

// Both parameters and return types are strongly typed.
var guid = await hubProxy.HubMethod2("message", 99);

// Client method registration is also strongly typed, so it's safe and easy.
var subscription = connection.Register<IReceiver>(new Receiver());

// Defining interfaces are useful not only for the client-side but also for the server-side.
// See Usage in this README.md for details.
interface IHub
{
    Task HubMethod1();
    Task<Guid> HubMethod2(string message, int value);
}

interface IReceiver
{
    Task ClientMethod(string message, DateTime dateTime);
}

class Receiver : IReceiver
{
    // implementation
}

API

This Source Generator provides two extension methods and one interface.

static class HubConnectionExtensions
{
    THub CreateHubProxy<THub>(this HubConnection connection, CancellationToken cancellationToken = default){...}
    IDisposable Register<TReceiver>(this HubConnection connection, TReceiver receiver){...}
}

// An interface for observing SignalR events.
interface IHubConnectionObserver
{
    Task OnClosed(Exception? exception);
    Task OnReconnected(string? connectionId);
    Task OnReconnecting(Exception? exception);
}

Use it as follows.

HubConnection connection = ...;

IHub hub = connection.CreateHubProxy<IHub>();
IDisposable subscription = connection.Register<IReceiver>(new Receiver());

Usage

For example, we have the following interface defined.

public class UserDefinedType
{
    public Guid Id { get; set; }
    public DateTime Datetime { get; set; }
}

// The return type of methods on the client-side must be Task. 
public interface IClientContract
{
    // Of course, user defined type is OK. 
    Task ClientMethod1(string user, string message, UserDefinedType userDefine);
    Task ClientMethod2();
}

// The return type of methods 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
{
    // implementation
}

class Receiver2 : IClientContract, IHubConnectionObserver
{
    // implementation
}

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());

// Invoke hub methods
hub.HubMethod1("user", "message");

// Unregister the receiver
subscription.Dispose();

Cancellation

In ASP.NET Core SignalR, CancellationToken is passed for each invoke.

On the other hand, in TypedSignalR.Client, CancellationToken is passed only once when creating a hub proxy. The passed CancelationToken will be used for each invoke internally.

var cts = new CancellationTokenSource();

// The following two are equivalent.

// 1: ASP.NET Core SignalR Client
var ret =  await connection.InvokeAsync<string>("HubMethod1", "user", "message", cts.Token);
await connection.InvokeAsync("HubMethod2", cts.Token);

// 2: TypedSignalR.Client
var hubProxy = connection.CreateHubProxy<IHubContract>(cts.Token);
var ret = await hubProxy.HubMethod1("user", "message");
await hubProxy.HubMethod2();

Server

Using the interface definitions, we can write as follows on the server-side (ASP.NET Core). TypedSignalR.Client is not necessary.

using Microsoft.AspNetCore.SignalR;

public class SomeHub : Hub<IClientContract>, IHubContract
{
    public async Task<string> HubMethod1(string user, string message)
    {
        var instance = new UserDefinedType()
        {
            Id = Guid.NewGuid(),
            DateTime = DateTime.Now,
        };

        // broadcast
        await this.Clients.All.ClientMethod1(user, message, instance);
        return "OK!";
    }

    public async Task HubMethod2()
    {
        await this.Clients.Caller.ClientMethod2();
    }
}

Recommendation

Sharing a Project

I recommend that these interfaces be shared between the client-side and server-side project, for example, by project references.

server.csproj --> shared.csproj <-- client.csproj

Client Code Format

It is easier to handle if we write client code in the following format.

class Client : IReceiver, IHubConnectionObserver, IDisposable
{
    private readonly IHub _hubProxy;
    private readonly IDisposable _subscription;
    private readonly CancellationTokenSource _cancellationTokenSource = new();

    public Client(HubConnection connection)
    {
        _hubProxy = connection.CreateHubProxy<IHub>(_cancellationTokenSource.Token);
        _subscription = connection.Register<IReceiver>(this);
    }

    // implementation
}

Streaming Support

SignalR supports both server-to-client streaming and client-to-server streaming.

TypedSignalR.Client supports both server-to-client streaming and client-to-server streaming. If you use IAsyncEnumerable<T>, Task<IAsyncEnumerable<T>>, or Task<ChannelReader<T>> for the method return type, it is analyzed as server-to-client streaming. And if IAsyncEnumerable<T> or ChannelReader<T> is used in the method parameter, it is analyzed as client-to-server streaming.

When using server-to-client streaming, a single CancellationToken can be used as a method parameter (Note: CancellationToken cannot be used as a parameter except for server-to-client streaming).

Client Results Support

.NET 7 and later, you can use client results.

TypedSignalR.Client supports client results. If you use Task<T> for the method return type in the receiver interface, you can use client results.

Compile-Time Error Support

This library has some restrictions, including those that come from server-side implementations.

  • Type argument of the CreateHubProxy/Register method must be an interface.
  • Only method definitions are allowed in the interface used for CreateHubProxy/Register.
    • It is forbidden to define properties and events.
  • The return type of the method in the interface used for CreateHubProxy must be Task or Task<T>.
  • The return type of the method in the interface used for Register must be Task.

It is complicated for humans to comply with these restrictions properly. So, this library looks for parts that do not follow the restriction and report detailed errors at compile time. Therefore, no runtime error occurs.

compile-time-error

Generated Source Code

TypedSignalR.Client checks the type argument of a methods CreateHubProxy and Register and generates source code. Generated source code can be seen in Visual Studio.

generated-code-visible-from-solution-explorer

Related Work

typedsignalr.client's People

Contributors

dependabot[bot] avatar nenonaninu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

typedsignalr.client's Issues

Nullable as parameter gives CS8639

Thanks for fixing nullables as return value.
But I also have nullables as parameters, and now with 3.5.0 I get a compile error instead:
CS8639 The typeof operator cannot be used on a nullable reference type
So the warnings before were better than the errors now :(

Registering from a razor page

From a Blazor Razor page, when registering the client interface:

var subscription = hubConnection.Register<IDatafeedClient>(this);

This exception is thrown:

System.InvalidOperationException: Failed to register a receiver. TypedSignalR.Client did not generate source code to register a receiver, which type is Trading.Abstractions.Datafeed.IDatafeedClient.
at TypedSignalR.Client.HubConnectionExtensions.Register[TReceiver](HubConnection connection, TReceiver receiver) in \TypedSignalR.Client\TypedSignalR.Client.SourceGenerator\TypedSignalR.Client.HubConnectionExtensions.Generated.cs:line 38
at Trading.IntlWeb.Pages.SignalRClientTest.OnInitializedAsync() in \Trading.IntlWeb\Pages\SignalRClientTest.razor:line 30
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.g__ProcessAsynchronousWork|48_0()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.WaitForQuiescence()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderRootComponentAsync(Int32 componentId, ParameterView initialParameters)
at Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.RenderComponentAsync(Type componentType, ParameterView initialParameters)
at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<>c__11`1.<b__11_0>d.MoveNext()

Why would this occur?

Support client results (.NET 7)

.NET Blog described the following statement.

Previously, when using SignalR, the server could invoke a method on a client but didn’t have the ability to wait for a response. This scenario is now supported with .NET 7 Preview 4.

I would like to support this new feature when it enters the official release of .NET 7.

Usage in Unity

We are trying to use this in a Unity project. We are importing the DLL like this:
https://docs.unity3d.com/Manual/roslyn-analyzers.html

However, Unity complains that it cannot find the namespace and types contained in the TypedSignalR.Client.dll.
Do you have any suggestions on how to use it with Unity?

We also tried to manually add the DLL to the C# compiler (csc.rsp) but also no luck there.

CancellationToken per request

As the documentation says

In ASP.NET Core SignalR, CancellationToken is passed for each invoke.

On the other hand, in TypedSignalR.Client, CancellationToken is passed only once when creating a hub proxy. The passed CancelationToken will be used for each invoke internally.

Why is it? In my application I store a long living client and use it for independent call chains.
It would be great if one could supply a CancellationToken for a specific request. It would be great if every method had a CancellationToken? cancellationToken = null parameter. If it is null, the request can use the global cancellation token (or combine the 2)

Is there a specific reason there is no per request cancellation token?

ps: This is such a great library, thank you for maintaining it!

Generated types are ignoring nullable option for arguments

When using a nullable argument on a client method, the generated types are missing the nullable specifier.

Example

public interface IChatHub
{
    Task SendMessage(string user, string? message);
}

public interface IChatHubClient
{
    Task ReceiveMessage(string user, string? message);
}

public class ChatHub : Hub<IChatHubClient>, IChatHub
{
    public async Task SendMessage(string user, string? message)
    {
        await Clients.All.ReceiveMessage(user, message);
    }
}

public class Program
{
    public async Task Start()
    {
        HubConnection connection = new HubConnectionBuilder()
            .WithUrl("/hello/world")
            .WithAutomaticReconnect()
            .Build();

        IChatHubClient hubProxy = connection.CreateHubProxy<IChatHubClient>();

        await connection.StartAsync();
    }
}

This generates the following ReceiveMessage type

image

I would expect the message argument to be nullable here.

Are nullable arguments not supported by TypedSignalR? Or is this a bug?

IHub does not inherit methods

First of all, thanks for your awesome work.

The following usecase does not work:

interface IHub : IAnyInterface {}

interface IAnyInterface
{
    Task<Guid> HubMethod2(string message, int value);
}

var hub = hubConnection.CreateHubProxy<IHub>();

build output:

Error	CS0535	'Extensions.HubInvokerForXyServicesIHub' does not implement interface member 'IAnyInterface.HubMethod2(string, int)'	<path>\TypedSignalR.Client\TypedSignalR.Client.SourceGenerator.ExtensionsSourceGenerator\TypedSignalR.Client.Extensions.Internal.Generated.cs

Escaped reserved identifiers cause issues in generated code

Great job there! Thanks for your effort.
One remark though: I have spent quite a lot of time to figure out why the generated code is full with strange errors, while I have followed the instructions you gave. It turned out that the generator could not handle my method parameter with the name @event. The generated code stripped the monkey, which resulted in quite a mess.

image

Add ClientFactory Functionality

I think adding ClientFactory functionality would be really helpful. Similar to how IHttpClientFactory can be used. The client could then be injected into the Program.cs and accessed via DI throughout the app. I have been trying to find how to manually inject the HubConnection on the client side. I would be happy to collaborate on something like this. I believe that SignalR is held back by it's client api.

Similar to this:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-8.0
And how Refit does for HttpClients.

Source code not generated

Want to use this nice library, but do receive the following error TypedSignalR.Client did not generate source code to create a hub proxy, which type is MyNamespace.ITestHub.

I tried to follow the instructions, my simple usage:

var url = new Uri(_settings.BackendUri, hubRoute);
var hubConnection = new HubConnectionBuilder()
   .WithUrl(url, options =>
   {
       options.AccessTokenProvider = async () =>
       {
           var tokenProvider = serviceProvider.GetRequiredService<IAccessTokenProvider>();
           var tokenResult = await tokenProvider.RequestAccessToken();
           if (tokenResult.TryGetToken(out var token))
           {
               return token.Value;
           }  
           throw new InvalidOperationException("Access token not available");
       };
   })
   .WithAutomaticReconnect()
   .Build();
await hubConnection.StartAsync();
var hub = hubConnection.Connection.CreateHubProxy<ITestHub>(); // <-- Exception here
var subscription = hubConnection.Connection.Register<ITestHubClient>(this);

The interface is totally basic:

public interface ITestHub
{
    public Task SendMessage(TestMessage message);
}

Would appreciate some pointers on what I'm doing wrong here.

Rethinking accessibility

It seems that internal is more appropriate than public for the accessibility of the generated codes.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.