Azure / azure-functions-signalrservice-extension

Azure Functions bindings for SignalR Service. Project moved to https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService .
MIT License
97 stars 46 forks source link

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

Open lfshr opened 2 years ago

lfshr commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

Y-Sindo commented 2 years ago

@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.