Search code examples
c#asp.net-coresignalripcrpc

Is there SignalR alternative with "return value to server" functionality?


My goal: Pass data do specific client who is connected to server and get results without calling Server method.

I tried use SignalR to do this (because It's very easy tool for me), but I can't get results (now I know why). I am working on ASP.NET Core 3.1.

My question: Is there SignalR alternative with "return value to server" functionality (call method with params on target client and get results)?


Solution

  • SignalR is usually used in a setup where there are multiple clients and a single server the clients connect to. This makes it a normal thing for clients to call the server and expect results back. Since the server usually does not really care about what individual clients are connected, and since the server usually broadcasts to a set of clients (e.g. using a group), the communication direction is mostly used for notifications or broadcasts. Single-target messages are possible but there isn’t a built-in mechanism for a request/response pattern.

    In order to make this work with SignalR you will need to have a way for the client to call back the server. So you will need a hub action to send the response to.

    That alone doesn’t make it difficult but what might do is that you will need to link a client-call with an incoming result message received by a hub. For that, you will have to build something.

    Here’s an example implementation to get you starting. The MyRequestClient is a singleton service that basically encapsulates the messaging and offers you an asynchronous method that will call the client and only complete once the client responded by calling the callback method on the hub:

    public class MyRequestClient
    {
        private readonly IHubContext<MyHub> _hubContext;
        private ConcurrentDictionary<Guid, object> _pendingTasks = new ConcurrentDictionary<Guid, object>();
    
        public MyRequestClient(IHubContext<MyHub> hubContext)
        {
            _hubContext = hubContext;
        }
    
        public async Task<int> Square(string connectionId, int number)
        {
            var requestId = Guid.NewGuid();
            var source = new TaskCompletionSource<int>();
            _pendingTasks[requestId] = source;
    
            await _hubContext.Clients.Client(connectionId).SendAsync("Square", nameof(MyHub.SquareCallback), requestId, number);
    
            return await source.Task;
        }
    
        public void SquareCallback(Guid requestId, int result)
        {
            if (_pendingTasks.TryRemove(requestId, out var obj) && obj is TaskCompletionSource<int> source)
                source.SetResult(result);
        }
    }
    

    In the hub, you then need the callback action to call the request client to complete the task:

    public class MyHub : Hub
    {
        private readonly ILogger<MyHub> _logger;
        private readonly MyRequestClient _requestClient;
    
        public MyHub(ILogger<MyHub> logger, MyRequestClient requestClient)
        {
            _logger = logger;
            _requestClient = requestClient;
        }
    
        public Task SquareCallback(Guid requestId, int number)
        {
            _requestClient.SquareCallback(requestId, number);
            return Task.CompletedTask;
        }
    
    
        // just for demo purposes
        public Task Start()
        {
            var connectionId = Context.ConnectionId;
            _ = Task.Run(async () =>
            {
                var number = 42;
                _logger.LogInformation("Starting Square: {Number}", number);
                var result = await _requestClient.Square(connectionId, number);
                _logger.LogInformation("Square returned: {Result}", result);
            });
            return Task.CompletedTask;
        }
    }
    

    The Start hub action is only for demo purposes to have a way to start this with a valid connection id.

    On the client, you then need to implement the client method and have it call the specified callback method once it’s done:

    connection.on('Square', (callbackMethod, requestId, number) => {
        const result = number * number;
        connection.invoke(callbackMethod, requestId, result);
    });
    

    Finally, you can try this out by invoking the Start action by a client:

    connection.invoke('Start');
    

    Of course, this implementation is very basic and will need a few things like proper error handling and support for timing out the tasks if the client didn’t respond properly. It would also be possible to expand this to support arbitrary calls, without having you to create all these methods manually (e.g. by having a single callback method on the hub that is able to complete any task).