dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.44k stars 10.03k forks source link

Is IAsyncEnumerable supported as a client result type in SignalR? #46161

Open RedwoodForest opened 1 year ago

RedwoodForest commented 1 year ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

I am trying to combine client results with streaming in SignalR to send a request message from the server to the client and have the client respond with a stream of response messages. Is this currently supported?

For example can I create a Hub<IClient> and IClient method Task<IAsyncEnumerable<PartialContent>> GetContent() and call it from the server on a client?

All the streaming examples I've found in the documentation are initiated by the client rather than the server.

Describe the solution you'd like

If it's not supported, then I'd like support to be added for returning streaming responses from the client to the server.

Additional context

No response

davidfowl commented 1 year ago

If it's not supported, then I'd like support to be added for returning streaming responses from the client to the server.

Can you outline the scenario? We've discussed it but the implementation is tricky so motivating examples would help.

KSemenenko commented 1 year ago

@RedwoodForest SignalR has Streams https://learn.microsoft.com/en-us/aspnet/core/signalr/streaming?view=aspnetcore-7.0 probably this is what you looking for

davidfowl commented 1 year ago

@KSemenenko this is about server to client streaming (which we don't support), initiated by the server.

KSemenenko commented 1 year ago

Now I get it, I thought for a while that these two-way streams. Anyway @davidfowl I have some examples for you:

Stock flow, client read somwhere this flow, maybe process it and send it to serverside. like 15k-50k messages per second.

Audio/video stream from client side to server. I did it once (I may be wrong. It was a couple of years ago and it didn't work very well), I sent audio buffer over regualr method, and play sound from Stream.

But perhaps these are all examples for which websockets are better without additional wrappers for better performance. Although once a stream has been created, there probably isn't much overhead anymore.

davidfowl commented 1 year ago

Stock flow, client read somwhere this flow, maybe process it and send it to serverside. like 15k-50k messages per second.

I don't understand this one.

Audio/video stream from client side to server. I did it once (I may be wrong. It was a couple of years ago and it didn't work very well), I sent audio buffer over regualr method, and play sound from Stream.

You can send anything really over SignalR now. Of course, there are optimizations you can make if you do it all yourself but the latest hub protocol is pretty good at transfering pretty much anything, since it support both binary and a text (JSON) protocol.

The benefit of client results if really keeping the state machine in a single async flow vs breaking the flow and state up into multiple calls over client and server that need to be correlated.

KSemenenko commented 1 year ago

In the first case, I was referring to stock trading, forex, or crypto. for example, clients read a stream of quotes from exchanges, then aggregate a it bit and then send it to the server. And you need a good speed there, also this is an endless stream of data.

Or, as an option, files can be transferred via signalR.

Someone in my team tried to do this. But something went wrong. =)

But as you can see, this is not a good examples. each case requires a special solution and imposes its own limitations...

RedwoodForest commented 1 year ago

@davidfowl

If it's not supported, then I'd like support to be added for returning streaming responses from the client to the server.

Can you outline the scenario? We've discussed it but the implementation is tricky so motivating examples would help.

In our case we have a .NET-based client and server for monitoring devices at remote sites and are using SignalR to add a feature to the client that allows occasional remote access via the user's browser to the device administration web pages for configuration by proxying HTTP traffic from the server to the client. (Access is limited to specific devices the user has permission to access remotely.)

There are other approaches to this one could take such as creating an SSH reverse proxy, but after exploring and prototyping a number of options SignalR will be the easiest to integrate into our existing client/server apps and the performance seems acceptable for our use case.

Streaming client results are not required for 95% of our use cases, as most HTTP responses can be handled by choosing an appropriate max message size, but some devices support things like downloading dumps of troubleshooting information which can be into the tens or hundreds of MB, and it would be nice to support this use case.

The benefit of client results if really keeping the state machine in a single async flow vs breaking the flow and state up into multiple calls over client and server that need to be correlated.

As you mention above, one workaround is to send the response from the client as a separate one-way streaming call and correlating it with the request on the server. We used this approach in our prototype before client results were supported, and it definitely works. The main downside is the additional complexity in waiting for the response and doing the correlation.

fishjimi commented 1 year ago

@KSemenenko this is about server to client streaming (which we don't support), initiated by the server.

This is a feature that I exactly wanted !!!!! I really hope SignalR could become into a more general framework for remote communication.

If it's not supported, then I'd like support to be added for returning streaming responses from the client to the server.

Can you outline the scenario? We've discussed it but the implementation is tricky so motivating examples would help.

For example, In IOT scenario , sometime server need to initiate request to device:

So, clients need to communicate with server in two-way real-time communication (best if request can be initiated by both side). There are many alternative options:

//Client can do this
var channel = Channel.CreateBounded<string>(10);
await connection.SendAsync("UploadStream", channel.Reader);
await channel.Writer.WriteAsync("some data");
await channel.Writer.WriteAsync("some more data");
channel.Writer.Complete();

//Client can do this
var cancellationTokenSource = new CancellationTokenSource();
var channel = await hubConnection.StreamAsChannelAsync<int>(
    "Counter", 10, 500, cancellationTokenSource.Token);
// Wait asynchronously for data to become available
while (await channel.WaitToReadAsync())
{
    // Read all currently available data synchronously, before waiting for more data
    while (channel.TryRead(out var count))
    {
        Console.WriteLine($"{count}");
    }
}

Console.WriteLine("Streaming completed");
public interface ITestHub
{
    Task<ChannelReader<string>> RequestClientToUploadStream();
    //Or Task<IAsyncEnumerable<string>> RequestClientToUploadStream();

    Task RequestClientToDownloadStream(ChannelReader<string> stream);
    //Or Task RequestClientToDownloadStream(IAsyncEnumerable<string> stream);
}

//Server can't do this
var stream = await _hubContext.Clients.Client(someConnectionId).RequestClientToUploadStream();
await foreach (var item in stream.ReadAllAsync())
{
    //...
}

//Server can't do this
var channel = Channel.CreateBounded<string>(10);
var stream = await _hubContext.Clients.Client(someConnectionId).RequestClientToDownloadStream(channel.Reader);
await channel.Writer.WriteAsync("some data");
await channel.Writer.WriteAsync("some more data");
channel.Writer.Complete();
davidfowl commented 1 year ago

@BrennanConroy how much work would it be to support streaming with client results? We hit a ton of gotchas implementing single results that would only be amplified by this (there are lots of foot guns), but I agree if they are strong scenarios that warrant it, we can investigate. If we did this, we'd start with single connections only, not groups etc.

For example, In IOT scenario , sometime server need to initiate request to device:

Retrieval device's status or files (such as logs,images,videos,recorded data,etc) Command device and return execution results Upgrade device's firmware

For sure, I have an example of command and control using client results https://github.com/davidfowl/CommandAndControl. These things were of course always possible before but was made trivial but adding client results. Still, seeing motivating, non-contrived examples (preferably with existing implementations) would be the best way to motivate this work.

fishjimi commented 1 year ago

For sure, I have an example of command and control using client results https://github.com/davidfowl/CommandAndControl. These things were of course always possible before but was made trivial but adding client results. Still, seeing motivating, non-contrived examples (preferably with existing implementations) would be the best way to motivate this work.

CommandAndControl is really cool! and it's pattern is really suit for IOT or game scenario. That's why I think SignalR is better(easy to use) than gRPC or something else, Mainlly becase it can initiate request from server, while orther frameworks can't. The best practices in my imagination is:

Orleans
+
SignalR (with streaming initiated by the server)
+
Orleans based SignalR backplane
[+Protobuf Serializer ]
\=
Will be able to be used in all scenarios 😂

KSemenenko commented 1 year ago

Orleans + SignalR (with streaming initiated by the server) + Orleans based SignalR backplane [+Protobuf Serializer ] = Will be able to be used in all scenarios 😂

try our library, we even made support for "server calls client method and get result" https://github.com/managedcode/Orleans.SignalR

A9G-Data-Droid commented 10 months ago

My example is a variable group of worker services. Each one processes data and stores some metadata about what it is doing in a local DB. The hub is a Blazor Server App that provides a central place to look at all the results of any or all workers. It is much like the command and control example, except the clients are returning tables that exceed the recommended message size by an unknown and growing amount. So I can't just increase the SignalR message size and pretend like it won't overrun some day.

I am currently streaming the data on a different connection because I can't stream in a client response. Then I return a simple confirmation in the client response. This gets me the client response flow I want and the streaming data to workaround the size limitation on the client response. It just seems odd. (You could argue that I am in this position due to architecture decisions but I don't want to roll a full distributed DB sync for this project at this point).

NOTE: I am using TypedSignalR.Client due to the lack of strongly typed client to hub communication.

/// A "client results" method in my client looks like this:
public async Task<int?> GetJobList()
{
    int? numberOfJobs = null;

    try
    {
        var jobList = _dbClient.GetAllThe<JobProgress>();
        numberOfJobs = jobList.Count;

        // I am recieving the SetCompleteJobList in a hub method that writes to _theJobList
        await _strongHub.SetCompleteJobList(jobList.ToAsyncEnumerable());
    }
    catch (Exception e)
    {
        _logger.Error(e, "Failed to send job list to hub!");
    }

    return numberOfJobs;
}

// From hub example:
var expectedJobCount = heyClient.GetJobList();
if (expectedJobCount == _theJobList.Count)
    Console.WriteLine("This is what success looks like.")