-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Using IAsyncEnumerable in the .NET Client #8935
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
Changes from all commits
2d43e6c
62d681e
6132874
4571de5
6c4cdae
d819f26
71ddc3b
c050a81
4c36bdb
48fd1a7
386cb77
0c51936
7148302
5ec5582
9f7d94a
69072ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| using Microsoft.AspNetCore.Connections.Features; | ||
| using Microsoft.AspNetCore.Internal; | ||
| using Microsoft.AspNetCore.SignalR.Client.Internal; | ||
| using Microsoft.AspNetCore.SignalR.Internal; | ||
| using Microsoft.AspNetCore.SignalR.Protocol; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Logging.Abstractions; | ||
|
|
@@ -436,6 +437,46 @@ private async Task StopAsyncCore(bool disposing) | |
| } | ||
| } | ||
|
|
||
| #if NETCOREAPP3_0 | ||
|
|
||
| /// <summary> | ||
| /// Invokes a streaming hub method on the server using the specified method name, return type and arguments. | ||
| /// </summary> | ||
| /// <typeparam name="TResult">The return type of the streaming server method.</typeparam> | ||
| /// <param name="methodName">The name of the server method to invoke.</param> | ||
| /// <param name="args">The arguments used to invoke the server method.</param> | ||
| /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None" />.</param> | ||
| /// <returns> | ||
| /// A <see cref="IAsyncEnumerable{TResult}"/> that represents the stream. | ||
| /// </returns> | ||
| public IAsyncEnumerable<TResult> StreamAsyncCore<TResult>(string methodName, object[] args, CancellationToken cancellationToken = default) | ||
| { | ||
| var cts = cancellationToken.CanBeCanceled ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : new CancellationTokenSource(); | ||
| var stream = CastIAsyncEnumerable<TResult>(methodName, args, cts); | ||
| var cancelableStream = AsyncEnumerableAdapters.MakeCancelableTypedAsyncEnumerable(stream, cts); | ||
| return cancelableStream; | ||
| } | ||
|
|
||
| private async IAsyncEnumerable<T> CastIAsyncEnumerable<T>(string methodName, object[] args, CancellationTokenSource cts) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe rename StreamAsAsyncEnumerableCore now |
||
| { | ||
| var reader = await StreamAsChannelCoreAsync(methodName, typeof(T), args, cts.Token); | ||
| try | ||
| { | ||
| while (await reader.WaitToReadAsync(cts.Token)) | ||
| { | ||
| while (reader.TryRead(out var item)) | ||
| { | ||
| yield return (T)item; | ||
| } | ||
| } | ||
| } | ||
mikaelm12 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| finally | ||
| { | ||
| cts.Dispose(); | ||
| } | ||
| } | ||
| #endif | ||
|
|
||
| private async Task<ChannelReader<object>> StreamAsChannelCoreAsyncCore(string methodName, Type returnType, object[] args, CancellationToken cancellationToken) | ||
| { | ||
| async Task OnStreamCanceled(InvocationRequest irq) | ||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -322,6 +322,203 @@ public async Task CanInvokeFromOnHandler(string protocolName, HttpTransportType | |
| } | ||
| } | ||
|
|
||
| [Theory] | ||
| [MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))] | ||
| [LogLevel(LogLevel.Trace)] | ||
| public async Task StreamAsyncCoreTest(string protocolName, HttpTransportType transportType, string path) | ||
| { | ||
| var protocol = HubProtocols[protocolName]; | ||
| using (StartServer<Startup>(out var server)) | ||
| { | ||
| var connection = CreateHubConnection(server.Url, path, transportType, protocol, LoggerFactory); | ||
| try | ||
| { | ||
| await connection.StartAsync().OrTimeout(); | ||
| var expectedValue = 0; | ||
| var streamTo = 5; | ||
| var asyncEnumerable = connection.StreamAsyncCore<int>("Stream", new object[] { streamTo }); | ||
| await foreach (var streamValue in asyncEnumerable) | ||
| { | ||
| Assert.Equal(expectedValue, streamValue); | ||
| expectedValue++; | ||
| } | ||
|
|
||
| Assert.Equal(streamTo, expectedValue); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); | ||
| throw; | ||
| } | ||
| finally | ||
| { | ||
| await connection.DisposeAsync().OrTimeout(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| [Theory] | ||
| [MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))] | ||
| [LogLevel(LogLevel.Trace)] | ||
| public async Task StreamAsyncTest(string protocolName, HttpTransportType transportType, string path) | ||
| { | ||
| var protocol = HubProtocols[protocolName]; | ||
| using (StartServer<Startup>(out var server)) | ||
| { | ||
| var connection = CreateHubConnection(server.Url, path, transportType, protocol, LoggerFactory); | ||
| try | ||
| { | ||
| await connection.StartAsync().OrTimeout(); | ||
| var expectedValue = 0; | ||
| var streamTo = 5; | ||
| var asyncEnumerable = connection.StreamAsync<int>("Stream", streamTo); | ||
| await foreach (var streamValue in asyncEnumerable) | ||
| { | ||
| Assert.Equal(expectedValue, streamValue); | ||
| expectedValue++; | ||
| } | ||
|
|
||
| Assert.Equal(streamTo, expectedValue); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); | ||
| throw; | ||
| } | ||
| finally | ||
| { | ||
| await connection.DisposeAsync().OrTimeout(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| [Theory] | ||
| [MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))] | ||
| [LogLevel(LogLevel.Trace)] | ||
| public async Task StreamAsyncDoesNotStartIfTokenAlreadyCanceled(string protocolName, HttpTransportType transportType, string path) | ||
| { | ||
| bool ExpectedErrors(WriteContext writeContext) | ||
| { | ||
| return writeContext.LoggerName == DefaultHubDispatcherLoggerName && | ||
| writeContext.EventId.Name == "FailedInvokingHubMethod"; | ||
| } | ||
| var protocol = HubProtocols[protocolName]; | ||
| using (StartServer<Startup>(out var server, ExpectedErrors)) | ||
| { | ||
| var connection = CreateHubConnection(server.Url, path, transportType, protocol, LoggerFactory); | ||
| try | ||
| { | ||
| await connection.StartAsync().OrTimeout(); | ||
|
|
||
| var cts = new CancellationTokenSource(); | ||
| cts.Cancel(); | ||
|
|
||
| var ex = Assert.ThrowsAsync<OperationCanceledException>(async () => | ||
| { | ||
| var stream = connection.StreamAsync<int>("Stream", 5, cts.Token); | ||
| await foreach (var streamValue in stream) | ||
| { | ||
| Assert.True(false, "Expected an exception from the streaming invocation."); | ||
| } | ||
| }); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); | ||
| throw; | ||
| } | ||
| finally | ||
| { | ||
| await connection.DisposeAsync().OrTimeout(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| [Theory] | ||
| [MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))] | ||
| [LogLevel(LogLevel.Trace)] | ||
| public async Task StreamAsyncCanBeCanceled(string protocolName, HttpTransportType transportType, string path) | ||
| { | ||
| var protocol = HubProtocols[protocolName]; | ||
| using (StartServer<Startup>(out var server)) | ||
| { | ||
| var connection = CreateHubConnection(server.Url, path, transportType, protocol, LoggerFactory); | ||
| try | ||
| { | ||
| await connection.StartAsync().OrTimeout(); | ||
|
|
||
| var cts = new CancellationTokenSource(); | ||
|
|
||
| var stream = connection.StreamAsync<int>("Stream", 5, cts.Token); | ||
| var results = new List<int>(); | ||
|
|
||
| var enumerator = stream.GetAsyncEnumerator(); | ||
| await Assert.ThrowsAsync<TaskCanceledException>(async () => | ||
| { | ||
| while (await enumerator.MoveNextAsync()) | ||
| { | ||
| results.Add(enumerator.Current); | ||
| cts.Cancel(); | ||
| } | ||
| }); | ||
|
|
||
| Assert.Single(results); | ||
| Assert.Equal(0, results[0]); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); | ||
| throw; | ||
| } | ||
| finally | ||
| { | ||
| await connection.DisposeAsync().OrTimeout(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| [Theory] | ||
| [MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))] | ||
| [LogLevel(LogLevel.Trace)] | ||
| public async Task StreamAsyncWithException(string protocolName, HttpTransportType transportType, string path) | ||
| { | ||
| bool ExpectedErrors(WriteContext writeContext) | ||
| { | ||
| return writeContext.LoggerName == DefaultHubDispatcherLoggerName && | ||
| writeContext.EventId.Name == "FailedInvokingHubMethod"; | ||
| } | ||
|
|
||
| var protocol = HubProtocols[protocolName]; | ||
| using (StartServer<Startup>(out var server, ExpectedErrors)) | ||
| { | ||
| var connection = CreateHubConnection(server.Url, path, transportType, protocol, LoggerFactory); | ||
| try | ||
| { | ||
| await connection.StartAsync().OrTimeout(); | ||
| var asyncEnumerable = connection.StreamAsync<int>("StreamException"); | ||
| var ex = await Assert.ThrowsAsync<HubException>(async () => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: OrTimeout |
||
| { | ||
| await foreach (var streamValue in asyncEnumerable) | ||
| { | ||
| Assert.True(false, "Expected an exception from the streaming invocation."); | ||
| } | ||
| }); | ||
|
|
||
| Assert.Equal("An unexpected error occurred invoking 'StreamException' on the server. InvalidOperationException: Error occurred while streaming.", ex.Message); | ||
|
|
||
| } | ||
| catch (Exception ex) | ||
| { | ||
| LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); | ||
| throw; | ||
| } | ||
| finally | ||
| { | ||
| await connection.DisposeAsync().OrTimeout(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
mikaelm12 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| [Theory] | ||
| [MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))] | ||
| [LogLevel(LogLevel.Trace)] | ||
|
|
@@ -465,6 +662,48 @@ public async Task CanStreamToAndFromClientInSameInvocation(string protocolName, | |
| } | ||
| } | ||
|
|
||
| [Theory] | ||
| [MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))] | ||
| [LogLevel(LogLevel.Trace)] | ||
| public async Task StreamAsyncCanBeCanceledThroughGetEnumerator(string protocolName, HttpTransportType transportType, string path) | ||
| { | ||
| var protocol = HubProtocols[protocolName]; | ||
| using (StartServer<Startup>(out var server)) | ||
| { | ||
| var connection = CreateHubConnection(server.Url, path, transportType, protocol, LoggerFactory); | ||
| try | ||
| { | ||
| await connection.StartAsync().OrTimeout(); | ||
| var stream = connection.StreamAsync<int>("Stream", 5 ); | ||
| var results = new List<int>(); | ||
|
|
||
| var cts = new CancellationTokenSource(); | ||
|
|
||
| var enumerator = stream.GetAsyncEnumerator(cts.Token); | ||
| await Assert.ThrowsAsync<TaskCanceledException>(async () => | ||
| { | ||
| while (await enumerator.MoveNextAsync()) | ||
| { | ||
| results.Add(enumerator.Current); | ||
| cts.Cancel(); | ||
| } | ||
| }); | ||
|
|
||
| Assert.Single(results); | ||
| Assert.Equal(0, results[0]); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); | ||
| throw; | ||
| } | ||
| finally | ||
| { | ||
| await connection.DisposeAsync().OrTimeout(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| [Theory] | ||
| [MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))] | ||
| [LogLevel(LogLevel.Trace)] | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.