aspnetcore: [Epic]: Support returning values from client invocations

Support server to clients acks so that reliable messaging can be implemented more easily. This would only be when using Clients.Client(). I think we should go back to the SendAsync (fire and forget) InvokeAsync (wait for ack) naming pattern. That’s the one sticking point.

EDIT by @anurse: For clarity, this issue is tracking all work related to allowing values to be returned from client invocations. It also covers allowing the server to wait for a void-returning (or Task-returning) client side method to complete.

Work left for .NET 7

  • #41777
    • What does an async .On method look like? Probably Single<T>
  • #41996 This was already an issue with Task returning .On methods, but client results likely makes it more likely to block on the client side [ ] #41997
    • Today we detect if you allow parallel hub invocations and throw if you don’t when trying to use the feature. This doesn’t work if you use IHubContext in the Hub, or if you have multiple waiting results for the same connections Hubs.
    • This is also especially bad in OnConnectedAsync because that’s a special method that runs before the receive loop starts, we need to throw/unblock/warn etc. for this [ ] Analyzer to warn about strongly-typed hubs and using InvokeAsync with .All, .Group, etc. [ ] InvokeAsync void result? Scenario, acks without needing a value [ ] [Scaleout] ServerA requests client result from connection on ServerB, ServerB goes down after receiving request, ServerA needs to know somehow so it can error the client result
  • [Scaleout] Consider alternative unique invocation ID generation instead of prepending the connectionID (https://github.com/dotnet/aspnetcore/pull/40811#discussion_r845643262) [ ] Look at performance The biggest performance issue I can think of right now is that RawResult allocates and copy the bytes which can be expensive [ ] Flow cancellation from server to client - Inject CancellationToken into .On methods and send CancelInvocation hub messages to clients

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 23
  • Comments: 66 (36 by maintainers)

Most upvoted comments

I have the same need

@brettclutch Wouldn’t the following do exactly what you want?

Server:
public MessageB MethodWhichReturnsMessageB(MessageA arg)
{
  // Do stuff
  return MessageB;
}

Client:
MessageB messageB = await InvokeAsync<MessageB>("MethodWhichReturnsMessageB", MessageA);

another +1 on this

Hey guys, I just tried this out with .NET 7 RC1 and the methods are still blocking (like before if one returned a Task). Are there any plans to change this behavior for the official .NET 7 release?

Done. #44014

The initial iteration of client return results is out and will be in preview4 bits.

Users

To use this new feature there are changes you need to make on the server side and the client side. On the server you need to use the new APIs on IHubContext or IHubCallerClients.

IHubContext<MyHub> context;
int arg = 10;
string result = await context.Single(connectionID).InvokeAsync<string>("ClientMethodName", arg);

or inside a Hub method, the caveat here is that you need to set the MaximumParallelInvocationsPerClient option on the server to be greater than 1, and there are still some issues with this that we’re working through in later previews:

public async Task HubMethod(string connectionId)
{
    int arg = 11;
    string result = await Clients.Single(connectionId).InvokeAsync<string>("ClientMethodName", arg);
}

Additionally, strongly-typed hubs can be used so now your interfaces can return a value and be used:

public interface IClient
{
    Task<string> Send(int arg);
}

public class HubT : Hub<IClient>
{
    public async Task HubMethod(string connectionId)
    {
        int arg = 12;
        string result = await Clients.Single(connectionId).Send(arg);
    }
}

And on the client you need to return a value from your .On(...) method: .NET Client:

hubConnection.On("ClientMethodName", (int someArg) =>
{
    return someArg.ToString();
});

TypeScript Client:

hubConnection.on("ClientMethodName", arg => {
    return `${arg}`;
});

HubLifetimeManager implementors

Three new methods have been added to HubLifetimeManager, they have default implementations (that throw NotImplementedExcetion) so that it’s not a breaking change to consume the new bits. Additionally, if the implementation of HubLifetimeManager communicates with other servers then you will need to be aware of the RawResult type which is used to pass the raw serialized bytes of a clients payload to another server without needed to fully deserialize the payload on the intermediary server.

public abstract class HubLifetimeManager<THub> where THub : Hub
{
+    public virtual Task<T> InvokeConnectionAsync<T>(string connectionId, string methodName, object?[] args, CancellationToken cancellationToken = default);
+    public virtual Task SetConnectionResultAsync(string connectionId, CompletionMessage result);
+    public virtual bool TryGetReturnType(string invocationId, [NotNullWhen(true)] out Type? type);
}

IHubProtocol implementors

The main change needed for IHubProtocol is to handle the RawResult type. IInvocationBinder.GetReturnType(string invocationID) can return RawResult which means don’t deserialize the result and instead store the raw bytes of the result in the RawResult type. And when serializing the payload if a RawResult type is being written write the raw bytes as if they were already serialized.

Example: Receiving Json of {"type":3,"result":34,"invocationId":"1234"} and seeing that result is RawResult will mean don’t try to deserialize 34 as an int or anything and instead store the bytes [(byte)'3', (byte)'4'] in a RawResult object. And when writing the Json of this RawResult object you wouldn’t serialize the byte[] as that would base64 encode the value, you instead directly write (byte)'3' and (byte)'4' to the output.

Work left for .NET 7

  • Add Java client implementation for client results
    • What does an async .On method look like? Probably Single<T>
  • HubConnection.On w/return results blocks client receive pipeline
    • This was already an issue with Task returning .On methods, but client results likely makes it more likely to block on the client side
      • .NET client blocks other .On handlers but does not block the receive loop
      • JS client doesn’t block anything, .On handlers for future invokes/sends can still run while blocked on user input, pings/etc. still received and processed
      • Java client blocks receive loop
  • InvokeAsync in Hub methods can soft-lock the connection
    • Today we detect if you allow parallel hub invocations and throw if you don’t when trying to use the feature. This doesn’t work if you use IHubContext in the Hub, or if you have multiple waiting results for the same connections Hubs.
    • This is also especially bad in OnConnectedAsync because that’s a special method that runs before the receive loop starts, we need to throw/unblock/warn etc. for this
  • Analyzer to warn about strongly-typed hubs and using InvokeAsync with .All, .Group, etc.
  • InvokeAsync void result? Scenario, acks without needing a value
  • [Scaleout] ServerA requests client result from connection on ServerB, ServerB goes down after receiving request, ServerA needs to know somehow so it can error the client result
  • [Scaleout] Consider alternative unique invocation ID generation instead of prepending the connectionID (https://github.com/dotnet/aspnetcore/pull/40811#discussion_r845643262)
  • Look at performance
    • The biggest performance issue I can think of right now is that RawResult allocates and copy the bytes which can be expensive
  • Flow cancellation from server to client
    • Inject CancellationToken into .On methods and send CancelInvocation hub messages to clients

Yep, we’ll either document it somewhere or see if we can make it error. Added it to the list above.

Ah yeah, thought about that scenario but didn’t look into it. It’s not going to work and likely never will work because of how OnConnectedAsync is called.

I like how easy they simply say “no plan to do it” when that “it” is mandatory feature even for a software 20 years ago.

another +1 on this

I use signalr to maintain proxy connections and much like @DavidErben I too could eliminate a lot of code required to direct responses.

Client connects to the master node and says I want this web address rendered this way the task is completed results put into response db queue then sent back to the client. It’s not bad but a lot of code could be eliminated if it was as simple as a client invocation.

Any news ? Is that really in. Net 5? Have you Any documentation link ?

* [ ]  `HubConnection.On` w/return results blocks client receive pipeline
  
  * This was already an issue with Task returning `.On` methods, but client results likely makes it more likely to block on the client side
    
    * .NET client blocks other `.On` handlers but does not block the receive loop

Hey guys, I just tried this out with .NET 7 RC1 and the methods are still blocking (like before if one returned a Task). Are there any plans to change this behavior for the official .NET 7 release?

We are using this feature to trigger commands on connected IoT devices. Usually a bunch of commands are triggered simultaneously, so it would be cool if they would run in parallel. But if it is not possible it is not a huge deal either.

Hey all we’re looking at this for .NET 7, can you post interesting scenarios that you doing today that would made easy with client acks/results?

Hey David,

we are using SignalR to manage a network of about 1500 IoT nodes (Raspberry Pis). We are invoking methods on the clients to read logs or configuration files, upload data or trigger commands. Right now we use an asynchronous approach so that we create a task object and store it in a database and then send it to the client. The client executes it and sends the response to an API endpoint which sets the status of the task to finished. Currently I am updating the server so that the client directly responds on the SignalR connection, but I need to manually sync tasks/threads to return the response within the same HTTP request (for the caller of my API). It works, but it is a bit clunky.

With return values it would be super easy for us to directly invoke commands on a client node and immediately receive the response. So definitely a +1 from my side for this enhancement.

Thanks for contacting us.

We’re moving this issue to the .NET 7 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it’s very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

There’s no news, this feature hasn’t been implemented

It works in 5.0.0.

another +1 on this