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

[Discussion] Improve developer experience for strongly typed hub proxies #279

Open
lfshr opened this issue Sep 27, 2021 · 3 comments
Open
Labels
enhancement New feature or request

Comments

@lfshr
Copy link

lfshr commented Sep 27, 2021

SignalR strongly typed hub proxies are a work in progress right now. Not to be confused with strongly typed clients, a strongly typed hub proxy will allow clients to invoke server methods with the same level of inference as servers can currently invoke client methods.

I thought it would be an idea to discuss what could be done on the Azure Functions side in preparation for this feature becoming available. As it stands I don't believe this feature could be easily implemented as Function triggers are part of the method signatures.

Since the proxy is implemented on the client, there isn't any need to wait for the PR to be completed. The primary constraint is that serverless hubs must implement SignalR message handlers that satisfy the contract of the hub interface; which can be achieved today.

@lfshr lfshr changed the title [Discussion] Improve developer experience for strongly typed hubs [Discussion] Improve developer experience for strongly typed hub proxies Sep 27, 2021
@Y-Sindo Y-Sindo added the enhancement New feature or request label Sep 27, 2021
@lfshr
Copy link
Author

lfshr commented Sep 27, 2021

To try and explain the issue a little better. This would be the current implementation of a ServerlessHub that has an interface to be used as a hub proxy. As you can see from the example, the Serverless Hub and the IMyHub interface are disconnected. There is no contract that can be made here.

// Consumed client-side: 
// var myHub = conn.GetProxy<IMyHub>();
// myHub.Broadcast("Hello World!");

public interface IMyHub
{
    Task Broadcast(string message);
    Task SendToGroup(string groupName, string message);
    Task SendToUser(string userName, string message);
    Task SendToConnection(string connectionId, string message);
    Task JoinGroup(string connectionId, string groupId);
    Task LeaveGroup(string connectionId, string groupId);
    Task JoinUserToGroup(string userName, string groupId);
    Task LeaveUserFromGroup(string userName, string groupId);
}

public class SimpleChat : ServerlessHub // Cannot simply extend IMyHub
{
    [FunctionAuthorize]
    [FunctionName(nameof(Broadcast))]
    public async Task Broadcast([SignalRTrigger]InvocationContext invocationContext, string message, ILogger logger)
    {
        await Clients.All.SendAsync(NewMessageTarget, new NewMessage(invocationContext, message));
        logger.LogInformation($"{invocationContext.ConnectionId} broadcast {message}");
    }

    [FunctionName(nameof(SendToGroup))]
    public async Task SendToGroup([SignalRTrigger]InvocationContext invocationContext, string groupName, string message)
    {
        await Clients.Group(groupName).SendAsync(NewMessageTarget, new NewMessage(invocationContext, message));
    }

    [FunctionName(nameof(SendToUser))]
    public async Task SendToUser([SignalRTrigger]InvocationContext invocationContext, string userName, string message)
    {
        await Clients.User(userName).SendAsync(NewMessageTarget, new NewMessage(invocationContext, message));
    }

    [FunctionName(nameof(SendToConnection))]
    public async Task SendToConnection([SignalRTrigger]InvocationContext invocationContext, string connectionId, string message)
    {
        await Clients.Client(connectionId).SendAsync(NewMessageTarget, new NewMessage(invocationContext, message));
    }

    [FunctionName(nameof(JoinGroup))]
    public async Task JoinGroup([SignalRTrigger]InvocationContext invocationContext, string connectionId, string groupName)
    {
        await Groups.AddToGroupAsync(connectionId, groupName);
    }

    [FunctionName(nameof(LeaveGroup))]
    public async Task LeaveGroup([SignalRTrigger]InvocationContext invocationContext, string connectionId, string groupName)
    {
        await Groups.RemoveFromGroupAsync(connectionId, groupName);
    }

    [FunctionName(nameof(JoinUserToGroup))]
    public async Task JoinUserToGroup([SignalRTrigger]InvocationContext invocationContext, string userName, string groupName)
    {
        await UserGroups.AddToGroupAsync(userName, groupName);
    }

    [FunctionName(nameof(LeaveUserFromGroup))]
    public async Task LeaveUserFromGroup([SignalRTrigger]InvocationContext invocationContext, string userName, string groupName)
    {
        await UserGroups.RemoveFromGroupAsync(userName, groupName);
    }
}

@Y-Sindo
Copy link
Member

Y-Sindo commented Sep 27, 2021

Thank you @lfshr, now I understand your question better.

@Y-Sindo
Copy link
Member

Y-Sindo commented Dec 10, 2021

@lfshr
An idea just comes to me. We don't need one trigger for each hub method. We can listen on one endpoint which is the upstream configured and dispatch all the upstream requests to the methods in a hub. You might want to see our serverless protocol.
Here is a very rough API design:

/// <summary>
/// This is a function scoped instance.
/// </summary>
public abstract class FunctionHub
{
    /// <summary>
    /// Gets the context of the function, such as logger. 
    /// </summary>
    public FunctionContext FunctionContext { get; }

    /// <summary>
    /// Gets the client invocation context such as the connection id, user name,
    /// </summary>
    public InvocationContext InvocationContext { get; }

    public virtual Task OnConnectedAsync()
    {
        return Task.CompletedTask;
    }

    public virtual Task OnDisconnectedAsync(Exception? exception)
    {
        return Task.CompletedTask;
    }
}

public class MyHub : FunctionHub, IMyHub
{
    /// <summary>
    /// You could even get dependencies injected from the constructor.
    /// </summary>
    public MyHub()
    {

    }
    public Task Broadcast(string message)
    {
        throw new NotImplementedException();
    }

    public Task SendToGroup(string groupName, string message)
    {
        throw new NotImplementedException();
    }

    public Task SendToUser(string userName, string message)
    {
        throw new NotImplementedException();
    }
}

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddFunctionHub<MyHub>(); //register the hub   
    }
}

Each client invocation comes, a MyHub instance is created and the method inside it will be executed. It is very similar to the Hub in ASP.NET.

The problem with this design is that it is not function-styled and cannot be used in other functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants