Skip to content

Conversation

@mikaelm12
Copy link
Contributor

@mikaelm12 mikaelm12 commented Mar 30, 2019

Fixes: #5376
We finally get to use the StreamAsync method name 😄

Currently what we have is

public static Task<ChannelReader<TResult>> StreamAsChannelAsync<TResult>(...)

This is adding support for

public static IAsyncEnumerable<TResult> StreamAsync<TResult>(...)

@mikaelm12 mikaelm12 changed the title Hacking IAsyncEnumerable into the .NET Client Using IAsyncEnumerable in the .NET Client Mar 30, 2019
}

#if NETCOREAPP3_0
public async IAsyncEnumerable<object> StreamAsyncCore(string methodName, Type returnType, object[] args, CancellationToken cancellationToken = default)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we just did

return await StreamAsChannelCoreAsync(methodName, returnType, args, cancellationToken).ReadAllAsync();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we should actually be doing here is using the MakeCancelableAsyncEnumerableFromChannel<T> method on the AsyncEnumerableAdapters class

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. We can just share the source for that file for now.

@Eilon Eilon added the area-signalr Includes: SignalR clients and servers label Mar 30, 2019
@Eilon
Copy link
Contributor

Eilon commented Mar 30, 2019

Tagging @divega because I know you're passionate about IAsyncEnumerable, so FYI.

public async IAsyncEnumerable<object> StreamAsyncCore(string methodName, Type returnType, object[] args, CancellationToken cancellationToken = default)
{
var stream = (await StreamAsChannelCoreAsyncCore(methodName, returnType, args, cancellationToken)).ReadAllAsync();
await foreach(var item in AsyncEnumerableAdapters.MakeCancelableAsyncEnumerable(stream, cancellationToken)) {
Copy link
Contributor

@analogrelay analogrelay Apr 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need the await foreach here anymore right? MakeCancelableAsyncEnumerable just returns the think we need. It can just be like this:

Suggested change
await foreach(var item in AsyncEnumerableAdapters.MakeCancelableAsyncEnumerable(stream, cancellationToken)) {
return AsyncEnumerableAdapters.MakeCancelableAsyncEnumerable(stream, cancellationToken);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah wait, I see why. Because that would end up being a Task<IAsyncEnumerable<T>> when we want an IAsyncEnumerable<T>. Unfortunate, but makes sense. Disregard!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are going to be doing this a lot (creating IAsyncEnumerable<T> from async methods), you could consider creating a helper like MakeCancelableAsyncEnumerableFromChannel but that takes a Task<Channel<T>> and handles it to an IAsyncEnumerable<T> that defers the await until the first call to MoveNextAsync. Then you can return that directly without creating an iterator method.

The only observable difference would be that any exception that awaiting StreamAsChannelCoreAsyncCore would cause will be deferred.

This is what we do with ExecuteDataReaderAsync.

Copy link
Member

@halter73 halter73 Apr 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a cool idea. Probably worth making a new issue to track it. We could convert Task<ChannelReader<T>> to ChannelReader<T> in the same way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's something we can look at if we encounter an issue. Let's get this rolling now and see how it lands :).

Copy link
Contributor

@divega divega Apr 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, one correction to what I said before: You want to pass a Func<Task<Channel<T>>> to allow deferring the call (in this case to StreamAsChannelCoreAsyncCore) completely instead of passing an already initiated Task<T>.

Probably worth making a new issue to track it.

Sure. If you see value. I am back to lurker mode 😄

public async IAsyncEnumerable<object> StreamAsyncCore(string methodName, Type returnType, object[] args, CancellationToken cancellationToken = default)
{
var stream = (await StreamAsChannelCoreAsyncCore(methodName, returnType, args, cancellationToken)).ReadAllAsync();
await foreach(var item in AsyncEnumerableAdapters.MakeCancelableAsyncEnumerable(stream, cancellationToken)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to do this?

Copy link
Member

@halter73 halter73 Apr 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of iterating over MakeCancelableAsyncEnumerable()'s return value, just return it directly.

You might want to create another version of the method which actually uses the CancellationToken passed to GetAsyncEnumerator(), and returns IAsyncEnumerable<TResult> instead of IAsyncEnumerable<object>.

Returning a IAsyncEnumerable<TResult> is actually easier since you can always directly return the value from ChannelReader<TResult>.ReadAllAsync().GetAsyncEnumerator() from CancelableAsyncEnumerable<TResult>.GetAsyncEnumerator() without worrying about wrapping the async enumerators for value types with BoxedAsyncEnumerator.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of iterating over MakeCancelableAsyncEnumerable()'s return value, just return it directly.

That doesn't really work because of the await of StreamAsChannelCoreAsyncCore beforehand though, right? That's why this has to be transformed into a generator using the yield syntax.

Copy link
Member

@halter73 halter73 Apr 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we return the IAsyncEnumerable directly, we'd have to make StreamAsync return Task<IAsyncEnumerable<TResult>> which @anurse and I decided isn't great. I think using an iterator method is OK then.

We still should use a version of MakeCancelableAsyncEnumerable<TResult> that returns IAsyncEnumerable<TResult> and uses the CancellationToken passed through to GetAsyncEnumerator(CancellationToken) linking it with the CancellationToken from StreamAsyncCore if both are non-default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task<IAsyncEnumerable> which @anurse and I decided isn't great. I think using an iterator method is OK then.

The problem with this is that if we use an iterator and yield return from the result of MakeCancelableAsyncEnumerable we actually lose the cancellation token which defeats the purpose.

We still should use a version of MakeCancelableAsyncEnumerable that returns IAsyncEnumerable

I worked down this route and realized that the streaming methods like StreamAsChannelCoreAsync all return ChannelReader<object>. This means that no matter what we would have to iterate and cast on every item.

@mikaelm12 mikaelm12 marked this pull request as ready for review April 2, 2019 19:19
@mikaelm12 mikaelm12 requested a review from halter73 as a code owner April 2, 2019 19:19
}

#if NETCOREAPP3_0
public async IAsyncEnumerable<object> StreamAsyncCore(string methodName, Type returnType, object[] args, CancellationToken cancellationToken = default)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this method generic and have it return IAsyncEnumerable<TResult> so you don't need to convert later in HubConnectionExtensions.StreamAsyncCore().

<Compile Include="$(SignalRSharedSourceRoot)PipeWriterStream.cs" Link="PipeWriterStream.cs" />
<Compile Include="$(SignalRSharedSourceRoot)ReflectionHelper.cs" Link="ReflectionHelper.cs" />
<Compile Include="$(SignalRSharedSourceRoot)TimerAwaitable.cs" Link="Internal\TimerAwaitable.cs" />
<Compile Include="$(SignalRRoot)server\Core\src\Internal\AsyncEnumerableAdapters.cs" Link="Internal\AsyncEnumerableAdapters.cs" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move AsyncEnumerableAdapters to SignalRSharedSourceRoot if you can.

Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like us to create and use a new AsyncEnumerableAdapters .MakeCancelableAsyncEnumerable<TResult>() method that uses the CancellationToken from GetAsyncEnumerator().

/// <returns>
/// A <see cref="IAsyncEnumerable{TResult}"/> that represents the stream.
/// </returns>
public async Task<IAsyncEnumerable<TResult>> StreamAsyncCore<TResult>(string methodName, object[] args, CancellationToken cancellationToken = default)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this to return a Task<IAsyncEnumerable> to preserve the cancellation logic from the CancelableTypedAsyncEnumerable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, I'd rather just return the IAsyncEnumerable but otherwise we lose the cancel logic

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this more?

Having StreamAsync return a Task<IAsyncEnumerable> is more consistent with StreamAsChannelAsync returning Task<ChannelReader<TResult>>, but in an ideal world where we had infinite development time neither would return a Task in 3.0.

Is there some reason cancellation cannot work if StreamAsync doesn't return a Task?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before we were doing something similar to

var stream= AsyncEnumerableAdapters.MakeCancelableTypedAsyncEnumerable(enumerable, cancellationToken)
await foreach(var item in stream)
{
    yield return stream
}

As I understand it this new AsyncEnumerable that from these returns wont have the previous cancellationToken that was passed in.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just move the call to StreamAsChannelCoreAsync into CastIAsyncEnumerable and await it at the beginning. Then StreamAsyncCore doesn't need to be async or Task-returning.

@mikaelm12
Copy link
Contributor Author

I would like us to create and use a new AsyncEnumerableAdapters .MakeCancelableAsyncEnumerable() method that uses the CancellationToken from GetAsyncEnumerator().

We found that the implementation of IAsyncEnumerrator doesn't do anything with the cancellation token passed into GetAsyncEnumerator which is why I couldn't get that test to pass @halter73

@mikaelm12
Copy link
Contributor Author

This is ready for review again

}

private async IAsyncEnumerable<T> CastIAsyncEnumerable<T>(ChannelReader<object> reader, CancellationTokenSource cts)
private async IAsyncEnumerable<T> CastIAsyncEnumerable<T>(string methodName, object[] args, CancellationTokenSource cts)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe rename StreamAsAsyncEnumerableCore now

@mikaelm12
Copy link
Contributor Author

@aspnet-hello
Copy link

This comment was made automatically. If there is a problem contact ryanbrandenburg.

I've triaged the above build.

@mikaelm12
Copy link
Contributor Author

@mikaelm12
Copy link
Contributor Author

@mikaelm12 mikaelm12 merged commit 6f197a9 into master Apr 6, 2019
@mikaelm12 mikaelm12 added the accepted This issue has completed "acceptance" testing (including accessibility) label Apr 29, 2019
@natemcmaster natemcmaster deleted the mikaelm12/ClientIAsyncEnumerable branch May 3, 2019 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accepted This issue has completed "acceptance" testing (including accessibility) area-signalr Includes: SignalR clients and servers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement support for IAsyncEnumerable in the .NET client

8 participants