Search code examples
signalrsignalr-hub

SignalR return value from client method


Hello I'm developing a Server-Client application that communicate with SignalR. What I have to implement is a mechanism that will allow my server to call method on client and get a result of that call. Both applications are developed with .Net Core.

My concept is, Server invokes a method on Client providing Id of that invocation, the client executes the method and in response calls the method on the Server with method result and provided Id so the Server can match the Invocation with the result.

Usage is looking like this:

var invocationResult = await Clients
        .Client(connectionId)
        .GetName(id)
        .AwaitInvocationResult<string>(ClientInvocationHelper._invocationResults, id);

AwaitInvocationResult - is a extension method to Task

  public static Task<TResultType> AwaitInvocationResult<TResultType>(this Task invoke, ConcurrentDictionary<string, object> lookupDirectory, InvocationId id)
    {
        return Task.Run(() =>
        {
            while (!ClientInvocationHelper._invocationResults.ContainsKey(id.Value)
                   || ClientInvocationHelper._invocationResults[id.Value] == null)
            {
                Thread.Sleep(500);
            }
            try
            {
                object data;
                var stingifyData = lookupDirectory[id.Value].ToString();
                //First we should check if invocation response contains exception
                if (IsClientInvocationException(stingifyData, out ClientInvocationException exception))
                {
                    throw exception;
                }
                if (typeof(TResultType) == typeof(string))
                {
                    data = lookupDirectory[id.Value].ToString();
                }
                else
                {
                    data = JsonConvert.DeserializeObject<TResultType>(stingifyData);
                }
                var result = (TResultType)data;
                return Task.FromResult(result);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        });
    }

As you can see basically I have a dictionary where key is invocation Id and value is a result of that invocation that the client can report. In a while loop I'm checking if the result is already available for server to consume, if it is, the result is converted to specific type.

This mechanism is working pretty well but I'm observing weird behaviour that I don't understand. If I call this method with await modifier the method in Hub that is responsible to receive a result from client is never invoked.

  ///This method gets called by the client to return a value of specific invocation
  public Task OnInvocationResult(InvocationId invocationId, object data)
  {
      ClientInvocationHelper._invocationResults[invocationId.Value] = data;
      return Task.CompletedTask;
  }

In result the while loop of AwaitInvocationResult never ends and the Hub is blocked.

Maby someone can explain this behaviour to me so I can change my approach or improve my code.


Solution

  • As it was mentioned in the answer by Brennan, before ASP.NET Core 5.0 SignalR connection was only able to handle one not streaming invocation of hub method at time. And since your invocation was blocked, server wasn't able to handle next invocation.

    But in this case you probably can try to handle client responses in separate hub like below.

    public class InvocationResultHandlerHub : Hub
    {
    
        public Task HandleResult(int invocationId, string result)
        {
            InvoctionHelper.SetResult(invocationId, result);
            return Task.CompletedTask;
        }
    }
    

    While hub method invocation is blocked, no other hub methods can be invoked by caller connection. But since client have separate connection for each hub, he will be able to invoke methods on other hubs. Probably not the best way, because client won't be able to reach first hub until response will be posted.

    Other way you can try is streaming invocations. Currently SignalR doesn't await them to handle next message, so server will handle invocations and other messages between streaming calls. You can check this behavior here in Invoke method, invocation isn't awaited when it is stream https://github.com/dotnet/aspnetcore/blob/c8994712d8c3c982111e4f1a09061998a81d68aa/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs#L371

    So you can try to add some dummy streaming parameter that you will not use:

        public async Task TriggerRequestWithResult(string resultToSend, IAsyncEnumerable<int> stream)
        {
            var invocationId = InvoctionHelper.ResolveInvocationId();
            await Clients.Caller.SendAsync("returnProvidedString", invocationId, resultToSend);
            var result = await InvoctionHelper.ActiveWaitForInvocationResult<string>(invocationId);
            Debug.WriteLine(result);
        }
    

    and on the client side you will also need to create and populate this parameter:

    var stringResult = document.getElementById("syncCallString").value;
    var dummySubject = new signalR.Subject();
    resultsConnection.invoke("TriggerRequestWithResult", stringResult, dummySubject);
    dummySubject.complete();
    

    More details: https://learn.microsoft.com/en-us/aspnet/core/signalr/streaming?view=aspnetcore-5.0

    If you can use ASP.NET Core 5, you can try to use new MaximumParallelInvocationsPerClient hub option. It will allow several invocations to execute in parallel for one connection. But if your client will call too much hub methods without providing result, connection will hang. More details: https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration?view=aspnetcore-5.0&tabs=dotnet

    Actually, since returning values from client invocations isn't implemented by SignalR, maybe you can try to look into streams to return values into hubs?