Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SignalR: Using IAsyncEnumerable<T> and ChannelReader<T> with ValueTypes in native AOT #56179

Closed
eerhardt opened this issue Jun 10, 2024 · 3 comments · Fixed by #56583
Closed
Labels
area-signalr Includes: SignalR clients and servers
Milestone

Comments

@eerhardt
Copy link
Member

With #56079, native AOT support for SignalR client was added. But one scenario that isn't supported is to use "streaming" APIs (IAsyncEnumerable<T> and ChannelReader<T>) with ValueTypes. In order to work with IAsyncEnumerable<T> and ChannelReader<T> we either need to "jump" to a <T> generic method (which is what it does today, and not supported with native AOT because it can't generate the code up front) or by using reflection to invoke the MoveNextAsync() and WaitToReadAsync() methods.

It was decided to not support this scenario until we get data that shows we need to implement this in SignalR.

Note that using ValueTypes in these scenarios isn't necessarily a performance gain because the T will be boxed (i.e. an allocation will occur) into the StreamItemMessage in:

while (!tokenSource.Token.IsCancellationRequested && reader.TryRead(out var item))
{
await SendWithLock(connectionState, new StreamItemMessage(streamId, item), tokenSource.Token).ConfigureAwait(false);

cc @BrennanConroy

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-signalr Includes: SignalR clients and servers label Jun 10, 2024
@BrennanConroy BrennanConroy added this to the Backlog milestone Jun 10, 2024
@BrennanConroy
Copy link
Member

Backlog, waiting for customer feedback.

@eerhardt
Copy link
Member Author

I found out that Blazor Server uses IAsyncEnumerable<ArraySegment<byte>> in its Hub:

public async IAsyncEnumerable<ArraySegment<byte>> SendDotNetStreamToJS(long streamId)

Which means Blazor Server won't work if we keep this limitation.

Given this information, we will need to support this scenario (at least for the server, but we might as well do the client as well). Right now, the only way I can see this working is by using pure reflection to read from the streaming objects.

@eerhardt eerhardt mentioned this issue Jun 26, 2024
1 task
@eerhardt
Copy link
Member Author

eerhardt commented Jul 2, 2024

There is a scenario here we can't solve without a source generator. Say I have a Hub defined like the following:

public class MyHub : Hub
{
    public async Task EnumerableValueTypeParameter(IAsyncEnumerable<int> source)
    {
        await foreach (var item in source)
        {
            // do something with item
        }
    }
}

The problem is that in order to invoke the MyHub.EnumerableValueTypeParameter method on the SignalR server, we need to be able to create an actual IAsyncEnumerable<int> instance on the server. We need a real instance of this type because we are going to pass it into the user-defined Hub's method. However, it isn't possible to create an instance of this type in native AOT using reflection. For classes / reference types, we can call MakeGenericMethod/Type to dynamically create the concrete object. But for a ValueType, it isn't guaranteed the AOT'd code for the ValueType will exist (and very likely won't exist since we call MakeGenericMethod on our own methods, which never get instantiated for the ValueType). The same applies for a ChannelReader<ValueType> parameter on the server as well.

For the other 3 cases, we can provide support on native AOT using reflection. The other 3 cases are:

  1. The server Hub's method returns an IAsyncEnumerable/ChannelReader of ValueType.
  2. On the client, passing in a parameter of IAsyncEnumerable/ChannelReader of ValueType.
    1. The difference here (and for a server Hub's method return value) is that the user's code is creating the IAsyncEnumerable/ChannelReader of ValueType. The SignalR library code just needs to read from it, which can be done using reflection.
  3. On the client, consuming a return value of IAsyncEnumerable/ChannelReader of ValueType.
    1. The difference here is that in order to consume the IAsyncEnumerable/ChannelReader, the user's code calls a generic method, passing in the type - IAsyncEnumerable<TResult> StreamAsync<TResult> or Task<ChannelReader<TResult>> StreamAsChannelAsync<TResult>. In this case, since we know the generic type, we can create a generic Channel<TResult> or have a generic class that implements IAsyncEnumerable<TResult>. There's no need for reflection/MakeGenericMethod.

For .NET 9 we are able to support these 3 cases. For the case on the server where a parameter of a Hub method takes an IAsyncEnumerable/ChannelReader of ValueType, we will continue throwing an exception for PublishAot=true.

eerhardt added a commit to eerhardt/aspnetcore that referenced this issue Jul 2, 2024
…ignalR native AOT

Support streaming ValueTypes from a SignalR Hub method in both the client and the server in native AOT. In order to make this work, we need to use pure reflection to read from the streaming object.

Support passing in an IAsyncEnumerable/ChannelReader of ValueType to a parameter in SignalR.Client. This works because the user code creates the concrete object, and the SignalR.Client library just needs to read from it using reflection.

The only scenario that can't be supported is on the SignalR server we can't support receiving an IAsyncEnumerable/ChannelReader of ValueType. This is because there is no way for the SignalR library code to construct a concrete instance to pass into the user-defined method on native AOT.

Fix dotnet#56179
eerhardt added a commit to eerhardt/aspnetcore that referenced this issue Jul 2, 2024
…ignalR native AOT

Support streaming ValueTypes from a SignalR Hub method in both the client and the server in native AOT. In order to make this work, we need to use pure reflection to read from the streaming object.

Support passing in an IAsyncEnumerable/ChannelReader of ValueType to a parameter in SignalR.Client. This works because the user code creates the concrete object, and the SignalR.Client library just needs to read from it using reflection.

The only scenario that can't be supported is on the SignalR server we can't support receiving an IAsyncEnumerable/ChannelReader of ValueType. This is because there is no way for the SignalR library code to construct a concrete instance to pass into the user-defined method on native AOT.

Fix dotnet#56179
@BrennanConroy BrennanConroy modified the milestones: Backlog, 9.0-preview7 Jul 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-signalr Includes: SignalR clients and servers
Projects
None yet
2 participants