diff --git a/.circleci/config.yml b/.circleci/config.yml index 7e3c5054..8594df23 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,15 +47,15 @@ jobs: - run: dotnet build --configuration Release - run: dotnet pack --configuration Release - store_artifacts: - path: ./src/GraphQL.Common/bin/Release/GraphQL.Common.2.0.0-alpha.3.nupkg + path: ./src/GraphQL.Common/bin/Release/GraphQL.Common.2.0.0-alpha.4.nupkg - store_artifacts: - path: ./src/GraphQL.Client/bin/Release/GraphQL.Client.2.0.0-alpha.3.nupkg + path: ./src/GraphQL.Client/bin/Release/GraphQL.Client.2.0.0-alpha.4.nupkg - deploy: name: MyGet command: | if [ "${CIRCLE_BRANCH}" == "master" ]; then - dotnet nuget push ./src/GraphQL.Common/bin/Release/GraphQL.Common.2.0.0-alpha.3.nupkg --api-key $MY_GET_API_KEY --source $MY_GET_SOURCE - dotnet nuget push ./src/GraphQL.Client/bin/Release/GraphQL.Client.2.0.0-alpha.3.nupkg --api-key $MY_GET_API_KEY --source $MY_GET_SOURCE + dotnet nuget push ./src/GraphQL.Common/bin/Release/GraphQL.Common.2.0.0-alpha.4.nupkg --api-key $MY_GET_API_KEY --source $MY_GET_SOURCE + dotnet nuget push ./src/GraphQL.Client/bin/Release/GraphQL.Client.2.0.0-alpha.4.nupkg --api-key $MY_GET_API_KEY --source $MY_GET_SOURCE fi workflows: version: 2 diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln index aa9d13e0..4d28e424 100644 --- a/GraphQL.Client.sln +++ b/GraphQL.Client.sln @@ -1,7 +1,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28407.52 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{47C98B55-08F1-4428-863E-2C5C876DEEFE}" ProjectSection(SolutionItems) = preProject @@ -45,6 +45,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9413 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Sample", "samples\GraphQL.Client.Sample\GraphQL.Client.Sample.csproj", "{B21E97C3-F328-473F-A054-A4BF272B63F0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{A2E950A3-BD50-40C0-8189-57B455FFBC62}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Common.Benchmark", "benchmarks\GraphQL.Common.Benchmark\GraphQL.Common.Benchmark.csproj", "{6A935C7C-7DD1-489D-9259-56C1A398D643}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Integration.Tests", "tests\GraphQL.Integration.Tests\GraphQL.Integration.Tests.csproj", "{86BC3878-6549-4EF1-9672-B7C15A3FDF46}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestServer", "tests\IntegrationTestServer\IntegrationTestServer.csproj", "{618653E5-41C2-4F17-BE4F-F904267500D4}" @@ -89,6 +93,10 @@ Global {D588BC10-6B17-477F-9546-F8D1CE02EACB}.Debug|Any CPU.Build.0 = Debug|Any CPU {D588BC10-6B17-477F-9546-F8D1CE02EACB}.Release|Any CPU.ActiveCfg = Release|Any CPU {D588BC10-6B17-477F-9546-F8D1CE02EACB}.Release|Any CPU.Build.0 = Release|Any CPU + {6A935C7C-7DD1-489D-9259-56C1A398D643}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A935C7C-7DD1-489D-9259-56C1A398D643}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A935C7C-7DD1-489D-9259-56C1A398D643}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A935C7C-7DD1-489D-9259-56C1A398D643}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -104,6 +112,7 @@ Global {86BC3878-6549-4EF1-9672-B7C15A3FDF46} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} {618653E5-41C2-4F17-BE4F-F904267500D4} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} {D588BC10-6B17-477F-9546-F8D1CE02EACB} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} + {6A935C7C-7DD1-489D-9259-56C1A398D643} = {A2E950A3-BD50-40C0-8189-57B455FFBC62} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4} diff --git a/README.md b/README.md index f7bfc120..68c7b303 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A GraphQL Client for .NET Standard over HTTP. ## Specification: The Library will try to follow the following standards and documents: -[GraphQL Specification](http://facebook.github.io/graphql/October2016/) +[GraphQL Specification](https://facebook.github.io/graphql/June2018/) [GraphQL HomePage](http://graphql.org/learn/) ## Usage: @@ -15,32 +15,30 @@ The Library will try to follow the following standards and documents: ### Create a GraphQLRequest: #### Simple Request: ```csharp -var heroRequest = new GraphQLRequest { - Query = @" +var heroRequest = new GraphQLRequest(@" { - hero { - name - } + hero { + name + } }" -}; +); ``` #### OperationName and Variables Request: ```csharp -var heroAndFriendsRequest = new GraphQLRequest { - Query = @" - query HeroNameAndFriends($episode: Episode) { - hero(episode: $episode) { - name - friends { - name - } - } - }", - OperationName = "HeroNameAndFriends", - Variables = new { - episode = "JEDI" - } +var heroAndFriendsRequest = new GraphQLRequest(@" + query HeroNameAndFriends($episode: Episode) { + hero(episode: $episode) { + name + friends { + name + } + } + }"){ + OperationName = "HeroNameAndFriends", + Variables = new { + episode = "JEDI" + } }; ``` diff --git a/SubscriptionIntegrationTest.ConsoleClient/Program.cs b/SubscriptionIntegrationTest.ConsoleClient/Program.cs index a891236a..95000512 100644 --- a/SubscriptionIntegrationTest.ConsoleClient/Program.cs +++ b/SubscriptionIntegrationTest.ConsoleClient/Program.cs @@ -1,4 +1,8 @@ using System; +using System.Net.WebSockets; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; using GraphQL.Client.Http; using GraphQL.Common.Request; @@ -6,39 +10,126 @@ namespace SubsccriptionIntegrationTest.ConsoleClient { class Program { - static void Main(string[] args) + static async Task Main(string[] args) { Console.WriteLine("configuring client ..."); - using (var client = new GraphQLHttpClient("http://localhost:5000/graphql/")) + using (var client = new GraphQLHttpClient("http://localhost:5000/graphql/", new GraphQLHttpClientOptions{ UseWebSocketForQueriesAndMutations = true })) { -#pragma warning disable 618 - var stream = client.CreateSubscriptionStream(new GraphQLRequest - { - Query = @" - subscription { - messageAdded{ - content - from { - displayName - } - } - }" - }, - e => Console.WriteLine($"WebSocketException: {e.Message} (WebSocketError {e.WebSocketErrorCode}, ErrorCode {e.ErrorCode}, NativeErrorCode {e.NativeErrorCode}")); -#pragma warning restore 618 Console.WriteLine("subscribing to message stream ..."); - using (var subscription = stream.Subscribe( - response => Console.WriteLine($"new message from \"{response.Data.messageAdded.from.displayName.Value}\": {response.Data.messageAdded.content.Value}"), - exception => Console.WriteLine($"message subscription stream failed: {exception}"), - () => Console.WriteLine($"message subscription stream completed"))) + + var subscriptions = new CompositeDisposable(); + + subscriptions.Add(client.WebSocketReceiveErrors.Subscribe(e => { + if(e is WebSocketException we) + Console.WriteLine($"WebSocketException: {we.Message} (WebSocketError {we.WebSocketErrorCode}, ErrorCode {we.ErrorCode}, NativeErrorCode {we.NativeErrorCode}"); + else + Console.WriteLine($"Exception in websocket receive stream: {e.ToString()}"); + })); + + subscriptions.Add(CreateSubscription("1", client)); + await Task.Delay(200); + subscriptions.Add(CreateSubscription2("2", client)); + await Task.Delay(200); + subscriptions.Add(CreateSubscription("3", client)); + await Task.Delay(200); + subscriptions.Add(CreateSubscription("4", client)); + await Task.Delay(200); + subscriptions.Add(CreateSubscription("5", client)); + await Task.Delay(200); + subscriptions.Add(CreateSubscription("6", client)); + await Task.Delay(200); + subscriptions.Add(CreateSubscription("7", client)); + + using (subscriptions) { Console.WriteLine("client setup complete"); - Console.WriteLine("press any key to exit"); - Console.Read(); + var quit = false; + do + { + Console.WriteLine("write message and press enter..."); + var message = Console.ReadLine(); + var graphQLRequest = new GraphQLRequest(@" + mutation($input: MessageInputType){ + addMessage(message: $input){ + content + } + }") + { + Variables = new + { + input = new + { + fromId = "2", + content = message, + sentAt = DateTime.Now + } + } + }; + var result = await client.SendMutationAsync(graphQLRequest).ConfigureAwait(false); + + if(result.Errors != null && result.Errors.Length > 0) + { + Console.WriteLine($"request returned {result.Errors.Length} errors:"); + foreach (var item in result.Errors) + { + Console.WriteLine($"{item.Message}"); + } + } + } + while(!quit); Console.WriteLine("shutting down ..."); } + Console.WriteLine("subscriptions disposed ..."); } + Console.WriteLine("client disposed ..."); + } + + private static IDisposable CreateSubscription(string id, GraphQLHttpClient client) + { +#pragma warning disable 618 + var stream = client.CreateSubscriptionStream(new GraphQLRequest(@" + subscription { + messageAdded{ + content + from { + displayName + } + } + }" + ) + { Variables = new { id } }); +#pragma warning restore 618 + + return stream.Subscribe( + response => Console.WriteLine($"{id}: new message from \"{response.Data.messageAdded.from.displayName.Value}\": {response.Data.messageAdded.content.Value}"), + exception => Console.WriteLine($"{id}: message subscription stream failed: {exception}"), + () => Console.WriteLine($"{id}: message subscription stream completed")); + + } + + + private static IDisposable CreateSubscription2(string id, GraphQLHttpClient client) + { +#pragma warning disable 618 + var stream = client.CreateSubscriptionStream(new GraphQLRequest(@" + subscription { + contentAdded{ + content + from { + displayName + } + } + }" + ) + { Variables = new { id } }); +#pragma warning restore 618 + + return stream.Subscribe( + response => Console.WriteLine($"{id}: new content from \"{response.Data.contentAdded.from.displayName.Value}\": {response.Data.contentAdded.content.Value}"), + exception => Console.WriteLine($"{id}: content subscription stream failed: {exception}"), + () => Console.WriteLine($"{id}: content subscription stream completed")); + } } } diff --git a/SubscriptionIntegrationTest.ConsoleClient/SubscriptionIntegrationTest.ConsoleClient.csproj b/SubscriptionIntegrationTest.ConsoleClient/SubscriptionIntegrationTest.ConsoleClient.csproj index 21901c2f..83d4884b 100644 --- a/SubscriptionIntegrationTest.ConsoleClient/SubscriptionIntegrationTest.ConsoleClient.csproj +++ b/SubscriptionIntegrationTest.ConsoleClient/SubscriptionIntegrationTest.ConsoleClient.csproj @@ -2,9 +2,14 @@ Exe - netcoreapp2.1 + netcoreapp3.0;net461 + 8.0 + + + + diff --git a/benchmarks/GraphQL.Common.Benchmark/GraphQL.Common.Benchmark.csproj b/benchmarks/GraphQL.Common.Benchmark/GraphQL.Common.Benchmark.csproj new file mode 100644 index 00000000..d5ccaa33 --- /dev/null +++ b/benchmarks/GraphQL.Common.Benchmark/GraphQL.Common.Benchmark.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp2.2 + + + + + + + + + + + diff --git a/benchmarks/GraphQL.Common.Benchmark/Program.cs b/benchmarks/GraphQL.Common.Benchmark/Program.cs new file mode 100644 index 00000000..cdbcfce2 --- /dev/null +++ b/benchmarks/GraphQL.Common.Benchmark/Program.cs @@ -0,0 +1,36 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using System; +using System.Security.Cryptography; + +namespace GraphQL.Common.Benchmark{ + + public class Md5VsSha256{ + + private const int N = 10000; + private readonly byte[] data; + + private readonly SHA256 sha256 = SHA256.Create(); + private readonly MD5 md5 = MD5.Create(); + + public Md5VsSha256(){ + this.data = new byte[N]; + new Random(42).NextBytes(this.data); + } + + [Benchmark] + public byte[] Sha256() => this.sha256.ComputeHash(this.data); + + [Benchmark] + public byte[] Md5() => this.md5.ComputeHash(this.data); + } + + public class Program{ + + public static void Main(string[] args){ + var summary = BenchmarkRunner.Run(); + } + + } + +} diff --git a/benchmarks/GraphQL.Common.Benchmark/Request/GraphQLRequestBenchmark.cs b/benchmarks/GraphQL.Common.Benchmark/Request/GraphQLRequestBenchmark.cs new file mode 100644 index 00000000..c6869149 --- /dev/null +++ b/benchmarks/GraphQL.Common.Benchmark/Request/GraphQLRequestBenchmark.cs @@ -0,0 +1,57 @@ +using System; +using System.Text; +using BenchmarkDotNet.Attributes; +using GraphQL.Common.Request; + +namespace GraphQL.Common.Benchmark.Request { + + public class GraphQLRequestBenchmark { + + private readonly string query; + private readonly string operationName; + private readonly dynamic variables; + private readonly GraphQLRequest graphQLRequest1 = Generate(); + private readonly GraphQLRequest graphQLRequest2 = Generate(); + + public GraphQLRequestBenchmark() { + var random = new Random(); + var data = new byte[1000]; + random.NextBytes(data); + this.query = Encoding.Default.GetString(data); + random.NextBytes(data); + this.operationName = Encoding.Default.GetString(data); + this.variables = new { + objectA = random.NextDouble(), + objectB = random.Next() + }; + } + + [Benchmark] + public GraphQLRequest Constructor() => new GraphQLRequest(this.query) { OperationName = this.operationName, Variables = this.variables }; + + [Benchmark] + public bool Equality() => this.graphQLRequest1.Equals(this.graphQLRequest2); + + [Benchmark] + public bool InEquality() => !this.graphQLRequest1.Equals(this.graphQLRequest2); + + private static GraphQLRequest Generate() { + var random = new Random(); + var data = new byte[1000]; + random.NextBytes(data); + var query = Encoding.Default.GetString(data); + random.NextBytes(data); + var operationName = Encoding.Default.GetString(data); + var variables = new { + objectA = random.NextDouble(), + objectB = random.Next() + }; + return new GraphQLRequest(query) { + OperationName = operationName, + Variables = variables + }; + } + + } + +} diff --git a/root.props b/root.props index d00301d1..b32dba6a 100644 --- a/root.props +++ b/root.props @@ -6,9 +6,9 @@ A GraphQL Client for .NET Standard True True - latest + 8.0 en-US - CS0618;CS1591;CS1701 + CS0618;CS1591;CS1701;NU5105;NU5125 https://raw.githubusercontent.com/graphql-dotnet/graphql-client/master/assets/logo.64x64.png https://raw.githubusercontent.com/graphql-dotnet/graphql-client/master/LICENSE.txt https://github.com/graphql-dotnet/graphql-client @@ -17,7 +17,7 @@ git https://github.com/graphql-dotnet/graphql-client.git True - 2.0.0-alpha.subscriptions-api.10 + 2.0.0-alpha.4.subscription-api.7 4 diff --git a/src/GraphQL.Client/GraphQL.Client.csproj b/src/GraphQL.Client/GraphQL.Client.csproj index 97a78f0e..8043bf35 100644 --- a/src/GraphQL.Client/GraphQL.Client.csproj +++ b/src/GraphQL.Client/GraphQL.Client.csproj @@ -14,6 +14,7 @@ + diff --git a/src/GraphQL.Client/Http/GraphQLHttpClient.cs b/src/GraphQL.Client/Http/GraphQLHttpClient.cs index 90209d40..e6a76359 100644 --- a/src/GraphQL.Client/Http/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/Http/GraphQLHttpClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Net.WebSockets; @@ -38,6 +39,10 @@ public GraphQLHttpClientOptions Options { set => this.graphQLHttpHandler.Options = value; } + /// + [Obsolete("EXPERIMENTAL")] + public IObservable WebSocketReceiveErrors => graphQlHttpWebSocket.ReceiveErrors; + #endregion internal readonly GraphQLHttpHandler graphQLHttpHandler; @@ -69,13 +74,14 @@ public GraphQLHttpClient(string endPoint, GraphQLHttpClientOptions options) : th /// The EndPoint to be used /// The Options to be used public GraphQLHttpClient(Uri endPoint, GraphQLHttpClientOptions options) { + + options.EndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint)); + if (options == null) { throw new ArgumentNullException(nameof(options)); } - if (options.EndPoint == null) { throw new ArgumentNullException(nameof(options.EndPoint)); } if (options.JsonSerializerSettings == null) { throw new ArgumentNullException(nameof(options.JsonSerializerSettings)); } if (options.HttpMessageHandler == null) { throw new ArgumentNullException(nameof(options.HttpMessageHandler)); } if (options.MediaType == null) { throw new ArgumentNullException(nameof(options.MediaType)); } - options.EndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint)); this.graphQLHttpHandler = new GraphQLHttpHandler(options); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(_getWebSocketUri(), options); } @@ -107,20 +113,28 @@ internal GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClie } public Task SendQueryAsync(string query, CancellationToken cancellationToken = default) => - this.SendQueryAsync(new GraphQLRequest { Query = query }, cancellationToken); + this.SendQueryAsync(new GraphQLRequest(query), cancellationToken); - public Task SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) => - this.graphQLHttpHandler.PostAsync(request, cancellationToken); + public Task SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) + { + return Options.UseWebSocketForQueriesAndMutations + ? this.graphQlHttpWebSocket.Request(request, cancellationToken) + : this.graphQLHttpHandler.PostAsync(request, cancellationToken); + } public Task SendMutationAsync(string query, CancellationToken cancellationToken = default) => - this.SendMutationAsync(new GraphQLRequest { Query = query }, cancellationToken); + this.SendMutationAsync(new GraphQLRequest(query), cancellationToken); - public Task SendMutationAsync(GraphQLRequest request, CancellationToken cancellationToken = default) => - this.graphQLHttpHandler.PostAsync(request, cancellationToken); + public Task SendMutationAsync(GraphQLRequest request, CancellationToken cancellationToken = default) + { + return Options.UseWebSocketForQueriesAndMutations + ? this.graphQlHttpWebSocket.Request(request, cancellationToken) + : this.graphQLHttpHandler.PostAsync(request, cancellationToken); + } [Obsolete("EXPERIMENTAL API")] public Task SendSubscribeAsync(string query, CancellationToken cancellationToken = default) => - this.SendSubscribeAsync(new GraphQLRequest { Query = query }, cancellationToken); + this.SendSubscribeAsync(new GraphQLRequest(query), cancellationToken); [Obsolete("EXPERIMENTAL API")] public Task SendSubscribeAsync(GraphQLRequest request, CancellationToken cancellationToken = default) @@ -152,14 +166,22 @@ public IObservable CreateSubscriptionStream(GraphQLRequest requ if (_disposed) throw new ObjectDisposedException(nameof(GraphQLHttpClient)); - return GraphQLHttpSubscriptionHelpers.CreateSubscriptionStream(request, graphQlHttpWebSocket, - Options, cancellationToken: _cancellationTokenSource.Token); + if (subscriptionStreams.ContainsKey(request)) + return subscriptionStreams[request]; + + var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, Options, cancellationToken: _cancellationTokenSource.Token); + + subscriptionStreams.Add(request, observable); + return observable; } /// [Obsolete("EXPERIMENTAL API")] public IObservable CreateSubscriptionStream(GraphQLRequest request, Action webSocketExceptionHandler) { + if (_disposed) + throw new ObjectDisposedException(nameof(GraphQLHttpClient)); + return CreateSubscriptionStream(request, e => { if (e is WebSocketException webSocketException) @@ -173,10 +195,19 @@ public IObservable CreateSubscriptionStream(GraphQLRequest requ [Obsolete("EXPERIMENTAL API")] public IObservable CreateSubscriptionStream(GraphQLRequest request, Action exceptionHandler) { - var observable = GraphQLHttpSubscriptionHelpers.CreateSubscriptionStream(request, graphQlHttpWebSocket, Options, exceptionHandler, _cancellationTokenSource.Token); + if (_disposed) + throw new ObjectDisposedException(nameof(GraphQLHttpClient)); + + if(subscriptionStreams.ContainsKey(request)) + return subscriptionStreams[request]; + + var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, Options, exceptionHandler, _cancellationTokenSource.Token); + subscriptionStreams.Add(request, observable); return observable; } + private Dictionary> subscriptionStreams = new Dictionary>(); + /// /// Releases unmanaged resources /// diff --git a/src/GraphQL.Client/Http/GraphQLHttpClientOptions.cs b/src/GraphQL.Client/Http/GraphQLHttpClientOptions.cs index 38008421..ddce13c7 100644 --- a/src/GraphQL.Client/Http/GraphQLHttpClientOptions.cs +++ b/src/GraphQL.Client/Http/GraphQLHttpClientOptions.cs @@ -49,6 +49,11 @@ public class GraphQLHttpClientOptions { var rnd = new Random(); return TimeSpan.FromSeconds(Math.Min(n, 5) * 1.5 + rnd.NextDouble()); }; + + /// + /// If , the websocket connection is also used for regular queries and mutations + /// + public bool UseWebSocketForQueriesAndMutations { get; set; } = false; } } diff --git a/src/GraphQL.Client/Http/GraphQLHttpSubscriptionHelpers.cs b/src/GraphQL.Client/Http/GraphQLHttpSubscriptionHelpers.cs index 512dc443..ef71a0ce 100644 --- a/src/GraphQL.Client/Http/GraphQLHttpSubscriptionHelpers.cs +++ b/src/GraphQL.Client/Http/GraphQLHttpSubscriptionHelpers.cs @@ -15,8 +15,8 @@ namespace GraphQL.Client.Http public static class GraphQLHttpSubscriptionHelpers { internal static IObservable CreateSubscriptionStream( + this GraphQLHttpWebSocket graphQlHttpWebSocket, GraphQLRequest request, - GraphQLHttpWebSocket graphQlHttpWebSocket, GraphQLHttpClientOptions options, Action exceptionHandler = null, CancellationToken cancellationToken = default) @@ -36,7 +36,9 @@ internal static IObservable CreateSubscriptionStream( Type = GQLWebSocketMessageType.GQL_STOP }; var observable = graphQlHttpWebSocket.ResponseStream - .Where(response => response.Id == startRequest.Id) + .Where(response => { + return response != null && response.Id == startRequest.Id; + }) .SelectMany(response => { switch (response.Type) @@ -107,11 +109,16 @@ internal static IObservable CreateSubscriptionStream( { try { - // if the external handler is not set, propagate all exceptions (default subscription behaviour without Retry()) - if (exceptionHandler == null) throw e; - - // exceptions thrown by the handler will propagate to OnError() - exceptionHandler?.Invoke(e); + if (exceptionHandler == null) { + // if the external handler is not set, propagate all exceptions except WebSocketExceptions + // this will ensure that the client tries to re-establish subscriptions on connection loss + if (!(e is WebSocketException)) throw e; + } + else + { + // exceptions thrown by the handler will propagate to OnError() + exceptionHandler?.Invoke(e); + } // throw exception on the observable to be caught by Retry() or complete sequence if cancellation was requested return cancellationToken.IsCancellationRequested @@ -140,5 +147,75 @@ internal static IObservable CreateSubscriptionStream( // transform to hot observable and auto-connect .Publish().RefCount(); } + + internal static async Task Request( + this GraphQLHttpWebSocket graphQlHttpWebSocket, + GraphQLRequest request, + CancellationToken cancellationToken = default) + { + return await Observable.Create(async observer => + { + var websocketRequest = new GraphQLWebSocketRequest + { + Id = Guid.NewGuid().ToString("N"), + Type = GQLWebSocketMessageType.GQL_START, + Payload = request + }; + var observable = graphQlHttpWebSocket.ResponseStream + .Where(response => { + return response != null && response.Id == websocketRequest.Id; + }) + .SelectMany(response => + { + switch (response.Type) + { + case GQLWebSocketMessageType.GQL_COMPLETE: + Debug.WriteLine($"received 'complete' message on request {websocketRequest.Id}"); + return Observable.Empty(); + case GQLWebSocketMessageType.GQL_ERROR: + Debug.WriteLine($"received 'error' message on request {websocketRequest.Id}"); + return Observable.Throw( + new GraphQLSubscriptionException(response.Payload)); + default: + Debug.WriteLine($"received response for request {websocketRequest.Id}"); + return Observable.Return(((JObject)response?.Payload) + ?.ToObject()); + } + }); + + try + { + // intialize websocket (completes immediately if socket is already open) + await graphQlHttpWebSocket.InitializeWebSocket().ConfigureAwait(false); + } + catch (Exception e) + { + // subscribe observer to failed observable + return Observable.Throw(e).Subscribe(observer); + } + + var disposable = new CompositeDisposable( + observable.Subscribe(observer) + ); + + Debug.WriteLine($"submitting request {websocketRequest.Id}"); + // send request + try + { + await graphQlHttpWebSocket.SendWebSocketRequest(websocketRequest).ConfigureAwait(false); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + return disposable; + }) + // complete sequence on OperationCanceledException, this is triggered by the cancellation token + .Catch(exception => + Observable.Empty()) + .FirstOrDefaultAsync(); + } } } diff --git a/src/GraphQL.Client/Http/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Http/GraphQLHttpWebSocket.cs index c5e2d349..90caf737 100644 --- a/src/GraphQL.Client/Http/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Http/GraphQLHttpWebSocket.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.IO; using System.Net.WebSockets; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -18,34 +19,64 @@ internal class GraphQLHttpWebSocket: IDisposable { private readonly Uri webSocketUri; private readonly GraphQLHttpClientOptions _options; - private readonly byte[] buffer = new byte[1024 * 1024]; - private readonly ArraySegment arraySegment; + private readonly ArraySegment buffer; private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private Subject _responseSubject = new Subject(); + private Subject _responseSubject; + private Subject _requestSubject = new Subject(); + private Subject _exceptionSubject = new Subject(); + private IDisposable _requestSubscription; public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None; - private ClientWebSocket clientWebSocket = null; + private WebSocket clientWebSocket = null; private int _connectionAttempt = 0; public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClientOptions options) { this.webSocketUri = webSocketUri; _options = options; - arraySegment = new ArraySegment(buffer); - ResponseStream = _createResponseStream(); + buffer = new ArraySegment(new byte[8192]); + _responseStream = _createResponseStream(); + + _requestSubscription = _requestSubject.Select(request => Observable.FromAsync(() => _sendWebSocketRequest(request))).Concat().Subscribe(); } - public IObservable ResponseStream { get; } + public IObservable ReceiveErrors => _exceptionSubject.AsObservable(); + public IObservable ResponseStream => _responseStream; + public IObservable _responseStream; + //private IDisposable _responseStreamConnection; - public async Task SendWebSocketRequest(GraphQLWebSocketRequest request) + public Task SendWebSocketRequest(GraphQLWebSocketRequest request) { - await InitializeWebSocket().ConfigureAwait(false); - var webSocketRequestString = JsonConvert.SerializeObject(request); - var arraySegmentWebSocketRequest = new ArraySegment(Encoding.UTF8.GetBytes(webSocketRequestString)); - await this.clientWebSocket.SendAsync(arraySegmentWebSocketRequest, WebSocketMessageType.Text, true, _cancellationTokenSource.Token).ConfigureAwait(false); + _requestSubject.OnNext(request); + return request.SendTask(); + } + + private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request) + { + try + { + if (_cancellationTokenSource.Token.IsCancellationRequested) + { + request.SendCanceled(); + return; + } + + await InitializeWebSocket().ConfigureAwait(false); + var webSocketRequestString = JsonConvert.SerializeObject(request); + await this.clientWebSocket.SendAsync( + new ArraySegment(Encoding.UTF8.GetBytes(webSocketRequestString)), + WebSocketMessageType.Text, + true, + _cancellationTokenSource.Token).ConfigureAwait(false); + request.SendCompleted(); + } + catch (Exception e) + { + request.SendFailed(e); + } } public Task InitializeWebSocketTask { get; private set; } = Task.CompletedTask; @@ -80,12 +111,27 @@ public Task InitializeWebSocket() return Task.CompletedTask; // else (re-)create websocket and connect + //_responseStreamConnection?.Dispose(); clientWebSocket?.Dispose(); - clientWebSocket = new ClientWebSocket(); - this.clientWebSocket.Options.AddSubProtocol("graphql-ws"); + + // fix websocket not supported on win 7 using + // https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed + clientWebSocket = SystemClientWebSocket.CreateClientWebSocket(); + switch (clientWebSocket) + { + case ClientWebSocket nativeWebSocket: + nativeWebSocket.Options.AddSubProtocol("graphql-ws"); + break; + case System.Net.WebSockets.Managed.ClientWebSocket managedWebSocket: + managedWebSocket.Options.AddSubProtocol("graphql-ws"); + break; + default: + throw new NotSupportedException($"unknown websocket type {clientWebSocket.GetType().Name}"); + } + return InitializeWebSocketTask = _connectAsync(_cancellationTokenSource.Token); } - + private IObservable _createResponseStream() { return Observable.Create(_createResultStream) @@ -96,10 +142,27 @@ private IObservable _createResponseStream() private async Task _createResultStream(IObserver observer, CancellationToken token) { - var observable = await _getReceiveResultStream().ConfigureAwait(false); + if (_responseSubject == null || _responseSubject.IsDisposed) + { + _responseSubject = new Subject(); + var observable = await _getReceiveResultStream().ConfigureAwait(false); + observable.Subscribe(_responseSubject); + + _responseSubject.Subscribe(_ => { }, ex => + { + _exceptionSubject.OnNext(ex); + _responseSubject?.Dispose(); + _responseSubject = null; + }, + () => { + _responseSubject?.Dispose(); + _responseSubject = null; + }); + } + return new CompositeDisposable ( - observable.Subscribe(observer), + _responseSubject.Subscribe(observer), Disposable.Create(() => { Debug.WriteLine("response stream disposed"); @@ -115,11 +178,20 @@ private async Task> _getReceiveResultStrea private async Task _connectAsync(CancellationToken token) { - await _backOff().ConfigureAwait(false); - Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); - await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); - Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}"); - _connectionAttempt = 1; + try + { + await _backOff().ConfigureAwait(false); + Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); + await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); + Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}"); + //_responseStreamConnection = _responseStream.Connect(); + _connectionAttempt = 1; + } + catch (Exception e) + { + _exceptionSubject.OnNext(e); + throw; + } } @@ -146,23 +218,42 @@ private async Task _receiveResultAsync() { try { - Debug.WriteLine("receiving websocket data ..."); - _cancellationTokenSource.Token.ThrowIfCancellationRequested(); - var webSocketReceiveResult = - await clientWebSocket.ReceiveAsync(arraySegment, CancellationToken.None).ConfigureAwait(false); - _cancellationTokenSource.Token.ThrowIfCancellationRequested(); - var stringResult = Encoding.UTF8.GetString(arraySegment.Array, 0, webSocketReceiveResult.Count); - return JsonConvert.DeserializeObject(stringResult); + Debug.WriteLine($"receiving data on websocket {clientWebSocket.GetHashCode()} ..."); + WebSocketReceiveResult webSocketReceiveResult = null; + + using (var ms = new MemoryStream()) + { + do + { + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); + ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); + } + while (!webSocketReceiveResult.EndOfMessage); + + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + ms.Seek(0, SeekOrigin.Begin); + + if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) + { + using (var reader = new StreamReader(ms, Encoding.UTF8)) + { + var stringResult = await reader.ReadToEndAsync(); + Debug.WriteLine($"data received on websocket {clientWebSocket.GetHashCode()}: {stringResult}"); + return JsonConvert.DeserializeObject(stringResult); + } + } + else + { + throw new NotSupportedException("binary websocket messages are not supported"); + } + } } catch (Exception e) { - Console.WriteLine(e); + Debug.WriteLine($"exception thrown while receiving websocket data: {e}"); throw; } - finally - { - Debug.WriteLine("websocket data received"); - } } private async Task _closeAsync(CancellationToken cancellationToken = default) diff --git a/src/GraphQL.Client/IGraphQLClient.cs b/src/GraphQL.Client/IGraphQLClient.cs index 60af36ab..098d0524 100644 --- a/src/GraphQL.Client/IGraphQLClient.cs +++ b/src/GraphQL.Client/IGraphQLClient.cs @@ -72,6 +72,12 @@ public interface IGraphQLClient : IDisposable { /// an observable stream for the specified subscription [Obsolete("EXPERIMENTAL")] IObservable CreateSubscriptionStream(GraphQLRequest request, Action exceptionHandler); + + /// + /// Publishes all exceptions which occur inside the websocket receive stream (i.e. for logging purposes) + /// + [Obsolete("EXPERIMENTAL")] + IObservable WebSocketReceiveErrors { get; } } } diff --git a/src/GraphQL.Client/Obsolete.GraphQLClient.Extensions.cs b/src/GraphQL.Client/Obsolete.GraphQLClient.Extensions.cs index a7b6dbfc..057e3c8a 100644 --- a/src/GraphQL.Client/Obsolete.GraphQLClient.Extensions.cs +++ b/src/GraphQL.Client/Obsolete.GraphQLClient.Extensions.cs @@ -99,8 +99,8 @@ fragment TypeRef on __Type { } }"; - private static readonly GraphQLRequest IntrospectionGraphQLRequest = new GraphQLRequest { - Query = IntrospectionQuery.Replace("\t", "").Replace("\n", "").Replace("\r", ""), + private static readonly GraphQLRequest IntrospectionGraphQLRequest = new GraphQLRequest( + IntrospectionQuery.Replace("\t", "").Replace("\n", "").Replace("\r", "")){ Variables = null }; diff --git a/src/GraphQL.Client/Obsolete.GraphQLClient.cs b/src/GraphQL.Client/Obsolete.GraphQLClient.cs index 73f81450..922927bf 100644 --- a/src/GraphQL.Client/Obsolete.GraphQLClient.cs +++ b/src/GraphQL.Client/Obsolete.GraphQLClient.cs @@ -53,7 +53,7 @@ public GraphQLClient(GraphQLClientOptions options) : base(options) { } /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// The Response public Task GetQueryAsync(string query, CancellationToken cancellationToken = default) => - this.GetAsync(new GraphQLRequest { Query = query }, cancellationToken); + this.GetAsync(new GraphQLRequest(query), cancellationToken); /// /// Send a via GET @@ -71,7 +71,7 @@ public Task GetAsync(GraphQLRequest request, CancellationToken /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// The Response public Task PostQueryAsync(string query, CancellationToken cancellationToken = default) => - this.PostAsync(new GraphQLRequest { Query = query }, cancellationToken); + this.PostAsync(new GraphQLRequest(query), cancellationToken); /// /// Send a via POST diff --git a/src/GraphQL.Common/GraphQL.Common.csproj b/src/GraphQL.Common/GraphQL.Common.csproj index ed75f9dd..116ff1b9 100644 --- a/src/GraphQL.Common/GraphQL.Common.csproj +++ b/src/GraphQL.Common/GraphQL.Common.csproj @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - netstandard1.0;netstandard2.0 + netstandard2.0 diff --git a/src/GraphQL.Common/Request/GraphQLRequest.cs b/src/GraphQL.Common/Request/GraphQLRequest.cs index 212a3642..d04f1992 100644 --- a/src/GraphQL.Common/Request/GraphQLRequest.cs +++ b/src/GraphQL.Common/Request/GraphQLRequest.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; @@ -7,7 +8,7 @@ namespace GraphQL.Common.Request { /// Represents a Query that can be fetched to a GraphQL Server. /// For more information /// - public class GraphQLRequest : IEquatable { + public class GraphQLRequest : IEquatable { /// /// The Query @@ -17,50 +18,60 @@ public class GraphQLRequest : IEquatable { /// /// If the provided contains multiple named operations, this specifies which operation should be executed. /// - public string OperationName { get; set; } + public string? OperationName { get; set; } /// /// The Variables /// - public dynamic Variables { get; set; } + public dynamic? Variables { get; set; } + + /// + /// Initialize a new GraphQLRequest + /// + /// The Query + public GraphQLRequest(string query){ + this.Query = query; + } + + /// + /// Initialize a new GraphQLRequest + /// + [Obsolete("Pass query as parameter! This constructor will be removed in a future version!")] + public GraphQLRequest() + { + this.Query = string.Empty; + } /// - public override bool Equals(object obj) => this.Equals(obj as GraphQLRequest); + public override bool Equals(object? obj) => this.Equals(obj as GraphQLRequest); /// - public bool Equals(GraphQLRequest other) { - if (other == null) { - return false; - } - if (ReferenceEquals(this, other)) { - return true; - } - if (!Equals(this.Query, other.Query)) { - return false; - } - if (!Equals(this.OperationName, other.OperationName)) { - return false; - } - if (!Equals(this.Variables, other.Variables)) { - return false; - } + public bool Equals(GraphQLRequest? other) { + if (other == null) {return false;} + if (ReferenceEquals(this, other)) { return true; } + if (!EqualityComparer.Default.Equals(this.Query, other.Query)) { return false; } + if (!EqualityComparer.Default.Equals(this.OperationName, other.OperationName)) { return false; } + if (!EqualityComparer.Default.Equals(this.Variables, other.Variables)) { return false; } return true; } /// - public override int GetHashCode() { - var hashCode = -689803966; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(this.Query); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(this.OperationName); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(this.Variables); - return hashCode; + public override int GetHashCode() + { + unchecked + { + var hashCode = EqualityComparer.Default.GetHashCode(Query); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(OperationName); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Variables); + return hashCode; + } } /// - public static bool operator ==(GraphQLRequest request1, GraphQLRequest request2) => EqualityComparer.Default.Equals(request1, request2); + public static bool operator ==(GraphQLRequest? request1, GraphQLRequest? request2) => EqualityComparer.Default.Equals(request1, request2); /// - public static bool operator !=(GraphQLRequest request1, GraphQLRequest request2) => !(request1 == request2); + public static bool operator !=(GraphQLRequest? request1, GraphQLRequest? request2) => !(request1 == request2); } diff --git a/src/GraphQL.Common/Request/GraphQLWebSocketRequest.cs b/src/GraphQL.Common/Request/GraphQLWebSocketRequest.cs index 46e14dce..d805efc4 100644 --- a/src/GraphQL.Common/Request/GraphQLWebSocketRequest.cs +++ b/src/GraphQL.Common/Request/GraphQLWebSocketRequest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace GraphQL.Common.Request { @@ -24,6 +25,30 @@ public class GraphQLWebSocketRequest : IEquatable { /// public GraphQLRequest Payload { get; set; } + private TaskCompletionSource _tcs = new TaskCompletionSource(); + + /// + /// Task used to await the actual send operation and to convey potential exceptions + /// + /// + public Task SendTask() => _tcs.Task; + + /// + /// gets called when the send operation for this request has completed sucessfully + /// + public void SendCompleted() => _tcs.SetResult(true); + + /// + /// gets called when an exception occurs during the send operation + /// + /// + public void SendFailed(Exception e) => _tcs.SetException(e); + + /// + /// gets called when the GraphQLHttpWebSocket has been disposed before the send operation for this request has started + /// + public void SendCanceled() => _tcs.SetCanceled(); + /// public override bool Equals(object obj) => this.Equals(obj as GraphQLWebSocketRequest); diff --git a/src/GraphQL.Common/Response/GraphQLLocation.cs b/src/GraphQL.Common/Response/GraphQLLocation.cs index 913a7a44..c8eb8c5c 100644 --- a/src/GraphQL.Common/Response/GraphQLLocation.cs +++ b/src/GraphQL.Common/Response/GraphQLLocation.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; @@ -6,7 +7,7 @@ namespace GraphQL.Common.Response { /// /// Represents the location where the has been found /// - public class GraphQLLocation : IEquatable { + public class GraphQLLocation : IEquatable { /// /// The Column @@ -19,38 +20,25 @@ public class GraphQLLocation : IEquatable { public uint Line { get; set; } /// - public override bool Equals(object obj) => this.Equals(obj as GraphQLLocation); + public override bool Equals(object? obj) => this.Equals(obj as GraphQLLocation); /// - public bool Equals(GraphQLLocation other) { - if (other == null) { - return false; - } - if (ReferenceEquals(this, other)) { - return true; - } - if (!Equals(this.Column, other.Column)) { - return false; - } - if (!Equals(this.Line, other.Line)) { - return false; - } + public bool Equals(GraphQLLocation? other) { + if (other == null) { return false; } + if (ReferenceEquals(this, other)) { return true; } + if (!EqualityComparer.Default.Equals(this.Column, other.Column)) { return false; } + if (!EqualityComparer.Default.Equals(this.Line, other.Line)) { return false; } return true; } /// - public override int GetHashCode() { - var hashCode = 412437926; - hashCode = hashCode * -1521134295 + this.Column.GetHashCode(); - hashCode = hashCode * -1521134295 + this.Line.GetHashCode(); - return hashCode; - } + public override int GetHashCode() => EqualityComparer.Default.GetHashCode(this); /// - public static bool operator ==(GraphQLLocation location1, GraphQLLocation location2) => EqualityComparer.Default.Equals(location1, location2); + public static bool operator ==(GraphQLLocation? location1, GraphQLLocation? location2) => EqualityComparer.Default.Equals(location1, location2); /// - public static bool operator !=(GraphQLLocation location1, GraphQLLocation location2) => !(location1 == location2); + public static bool operator !=(GraphQLLocation? location1, GraphQLLocation? location2) => !(location1 == location2); } diff --git a/tests/GraphQL.Client.Tests/GraphQL.Client.Tests.csproj b/tests/GraphQL.Client.Tests/GraphQL.Client.Tests.csproj index 660883af..1c9a24d9 100644 --- a/tests/GraphQL.Client.Tests/GraphQL.Client.Tests.csproj +++ b/tests/GraphQL.Client.Tests/GraphQL.Client.Tests.csproj @@ -4,16 +4,17 @@ + true netcoreapp3.0 - + - + diff --git a/tests/GraphQL.Client.Tests/GraphQLClientGetTests.cs b/tests/GraphQL.Client.Tests/GraphQLClientGetTests.cs index faad38e8..2cdc0dfa 100644 --- a/tests/GraphQL.Client.Tests/GraphQLClientGetTests.cs +++ b/tests/GraphQL.Client.Tests/GraphQLClientGetTests.cs @@ -8,14 +8,12 @@ public class GraphQLClientGetTests : BaseGraphQLClientTest { [Fact] public async void QueryGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" { person(personID: ""1"") { name } - }" - }; + }"); var response = await this.GraphQLClient.GetAsync(graphQLRequest).ConfigureAwait(false); Assert.Equal("Luke Skywalker", response.Data.person.name.Value); @@ -24,8 +22,7 @@ public async void QueryGetAsyncFact() { [Fact] public async void OperationNameGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person{ person(personID: ""1"") { name @@ -36,7 +33,7 @@ query Planet { planet(planetID: ""1"") { name } - }", + }") { OperationName = "Person" }; var response = await this.GraphQLClient.GetAsync(graphQLRequest).ConfigureAwait(false); @@ -47,13 +44,12 @@ query Planet { [Fact] public async void VariablesGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest (@" query Person($personId: ID!){ person(personID: $personId) { name } - }", + }") { Variables = new { personId = "1" } @@ -66,8 +62,7 @@ query Person($personId: ID!){ [Fact] public async void OperationNameVariableGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person($personId: ID!){ person(personID: $personId) { name @@ -78,7 +73,7 @@ query Planet { planet(planetID: ""1"") { name } - }", + }"){ OperationName = "Person", Variables = new { personId = "1" diff --git a/tests/GraphQL.Client.Tests/GraphQLClientPostTests.cs b/tests/GraphQL.Client.Tests/GraphQLClientPostTests.cs index ba56c9a1..aacc1587 100644 --- a/tests/GraphQL.Client.Tests/GraphQLClientPostTests.cs +++ b/tests/GraphQL.Client.Tests/GraphQLClientPostTests.cs @@ -9,14 +9,13 @@ public class GraphQLClientPostTests : BaseGraphQLClientTest { [Fact] public async void QueryPostAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" { person(personID: ""1"") { name } }" - }; + ); var response = await this.GraphQLClient.PostAsync(graphQLRequest).ConfigureAwait(false); Assert.Equal("Luke Skywalker", response.Data.person.name.Value); @@ -24,17 +23,14 @@ public async void QueryPostAsyncFact() { } [Fact] - public async void QueryPostAsyncWithoutUtf8EncodingFact() - { - var graphQLRequest = new GraphQLRequest - { - Query = @" + public async void QueryPostAsyncWithoutUtf8EncodingFact(){ + var graphQLRequest = new GraphQLRequest(@" { person(personID: ""1"") { name } }" - }; + ); this.GraphQLClient.Options.MediaType = MediaTypeHeaderValue.Parse("application/json"); var response = await this.GraphQLClient.PostAsync(graphQLRequest).ConfigureAwait(false); @@ -44,8 +40,7 @@ public async void QueryPostAsyncWithoutUtf8EncodingFact() [Fact] public async void OperationNamePostAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person{ person(personID: ""1"") { name @@ -56,7 +51,7 @@ query Planet { planet(planetID: ""1"") { name } - }", + }") { OperationName = "Person" }; var response = await this.GraphQLClient.PostAsync(graphQLRequest).ConfigureAwait(false); @@ -67,13 +62,12 @@ query Planet { [Fact] public async void VariablesPostAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person($personId: ID!){ person(personID: $personId) { name } - }", + }") { Variables = new { personId = "1" } @@ -86,8 +80,7 @@ query Person($personId: ID!){ [Fact] public async void OperationNameVariablePostAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person($personId: ID!){ person(personID: $personId) { name @@ -98,7 +91,7 @@ query Planet { planet(planetID: ""1"") { name } - }", + }") { OperationName = "Person", Variables = new { personId = "1" diff --git a/tests/GraphQL.Client.Tests/Http/GraphQLHttpClientSendQueryAsyncTest.cs b/tests/GraphQL.Client.Tests/Http/GraphQLHttpClientSendQueryAsyncTest.cs index 82062542..2d21100e 100644 --- a/tests/GraphQL.Client.Tests/Http/GraphQLHttpClientSendQueryAsyncTest.cs +++ b/tests/GraphQL.Client.Tests/Http/GraphQLHttpClientSendQueryAsyncTest.cs @@ -1,42 +1,36 @@ -using GraphQL.Client.Http; -using GraphQL.Common.Request; using System; -using System.Collections.Generic; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; +using GraphQL.Client.Http; +using GraphQL.Common.Request; using Xunit; namespace GraphQL.Client.Tests.Http { - public class GraphQLHttpClientSendQueryAsyncTest - { + + public class GraphQLHttpClientSendQueryAsyncTest{ // Relates to an issue with the 1.x versions. // See: https://github.com/graphql-dotnet/graphql-client/issues/53 [Fact] - public async void SendQueryAsyncShouldPreserveUriParametersFact() - { + public async void SendQueryAsyncShouldPreserveUriParametersFact(){ var endpoint = new Uri("http://localhost/api/graphql?code=my-secret-api-key"); var handlerStub = new HttpHandlerStub(); - GraphQLHttpClientOptions options = new GraphQLHttpClientOptions() - { + var options = new GraphQLHttpClientOptions(){ EndPoint = endpoint, HttpMessageHandler = handlerStub }; - GraphQLHttpClient systemUnderTest = new GraphQLHttpClient(options); + var systemUnderTest = new GraphQLHttpClient(options); - var response = await systemUnderTest.SendQueryAsync(new GraphQLRequest - { - Query = @" + var response = await systemUnderTest.SendQueryAsync(new GraphQLRequest(@" { person(personID: ""1"") { name } }" - }); + )); var actualRequestUri = handlerStub.LastRequest.RequestUri; var queryParams = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(actualRequestUri.Query); @@ -45,20 +39,21 @@ public async void SendQueryAsyncShouldPreserveUriParametersFact() } - private class HttpHandlerStub : HttpMessageHandler - { + private class HttpHandlerStub : HttpMessageHandler{ + public HttpRequestMessage LastRequest { get; private set; } public CancellationToken LastCancellationToken { get; private set; } - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - LastRequest = request; - LastCancellationToken = cancellationToken; - var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK); - response.Content = new StringContent("{}"); + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken){ + this.LastRequest = request; + this.LastCancellationToken = cancellationToken; + var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK){ + Content = new StringContent("{}") + }; return Task.FromResult(response); } + } } diff --git a/tests/GraphQL.Client.Tests/Http/HttpClientExtensionsTest.cs b/tests/GraphQL.Client.Tests/Http/HttpClientExtensionsTest.cs index 1738f0a2..607c7e28 100644 --- a/tests/GraphQL.Client.Tests/Http/HttpClientExtensionsTest.cs +++ b/tests/GraphQL.Client.Tests/Http/HttpClientExtensionsTest.cs @@ -13,14 +13,13 @@ public class HttpClientExtensionsTest : BaseGraphQLClientTest { [Fact] public async void QueryGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" { person(personID: ""1"") { name } }" - }; + ); var response = await this.GraphQLHttpClient.SendQueryAsync(graphQLRequest).ConfigureAwait(false); Assert.Equal("Luke Skywalker", response.Data.person.name.Value); @@ -29,8 +28,7 @@ public async void QueryGetAsyncFact() { [Fact] public async void OperationNameGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person{ person(personID: ""1"") { name @@ -41,7 +39,7 @@ query Planet { planet(planetID: ""1"") { name } - }", + }"){ OperationName = "Person" }; var response = await this.GraphQLHttpClient.SendQueryAsync(graphQLRequest).ConfigureAwait(false); @@ -52,13 +50,12 @@ query Planet { [Fact] public async void VariablesGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person($personId: ID!){ person(personID: $personId) { name } - }", + }") { Variables = new { personId = "1" } @@ -71,8 +68,7 @@ query Person($personId: ID!){ [Fact] public async void OperationNameVariableGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person($personId: ID!){ person(personID: $personId) { name @@ -83,7 +79,7 @@ query Planet { planet(planetID: ""1"") { name } - }", + }") { OperationName = "Person", Variables = new { personId = "1" diff --git a/tests/GraphQL.Client.Tests/IGraphQLClientTest.cs b/tests/GraphQL.Client.Tests/IGraphQLClientTest.cs index 7b5b6368..7c384d55 100644 --- a/tests/GraphQL.Client.Tests/IGraphQLClientTest.cs +++ b/tests/GraphQL.Client.Tests/IGraphQLClientTest.cs @@ -8,14 +8,13 @@ public class IGraphQLClientTest : BaseGraphQLClientTest { [Fact] public async void QueryGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" { person(personID: ""1"") { name } }" - }; + ); var response = await this.GraphQLClientSwapi.SendQueryAsync(graphQLRequest).ConfigureAwait(false); Assert.Equal("Luke Skywalker", response.Data.person.name.Value); @@ -24,8 +23,7 @@ public async void QueryGetAsyncFact() { [Fact] public async void OperationNameGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person{ person(personID: ""1"") { name @@ -36,7 +34,7 @@ query Planet { planet(planetID: ""1"") { name } - }", + }") { OperationName = "Person" }; var response = await this.GraphQLClientSwapi.SendQueryAsync(graphQLRequest).ConfigureAwait(false); @@ -47,13 +45,12 @@ query Planet { [Fact] public async void VariablesGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person($personId: ID!){ person(personID: $personId) { name } - }", + }") { Variables = new { personId = "1" } @@ -66,8 +63,7 @@ query Person($personId: ID!){ [Fact] public async void OperationNameVariableGetAsyncFact() { - var graphQLRequest = new GraphQLRequest { - Query = @" + var graphQLRequest = new GraphQLRequest(@" query Person($personId: ID!){ person(personID: $personId) { name @@ -78,7 +74,7 @@ query Planet { planet(planetID: ""1"") { name } - }", + }") { OperationName = "Person", Variables = new { personId = "1" diff --git a/tests/GraphQL.Common.Tests/AssertGraphQL.cs b/tests/GraphQL.Common.Tests/AssertGraphQL.cs index a81f38ad..4115fc9f 100644 --- a/tests/GraphQL.Common.Tests/AssertGraphQL.cs +++ b/tests/GraphQL.Common.Tests/AssertGraphQL.cs @@ -6,8 +6,6 @@ namespace GraphQL.Common.Tests { public static class AssertGraphQL { - public static void CorrectGraphQLRequest(GraphQLRequest graphQLRequest) => Assert.NotNull(graphQLRequest.Query); - public static void CorrectGraphQLResponse(GraphQLResponse graphQLResponse) { Assert.NotNull(graphQLResponse.Data); Assert.Null(graphQLResponse.Errors); diff --git a/tests/GraphQL.Common.Tests/GraphQL.Common.Tests.csproj b/tests/GraphQL.Common.Tests/GraphQL.Common.Tests.csproj index e0a660a7..4aae0487 100644 --- a/tests/GraphQL.Common.Tests/GraphQL.Common.Tests.csproj +++ b/tests/GraphQL.Common.Tests/GraphQL.Common.Tests.csproj @@ -1,9 +1,10 @@ - + + true netcoreapp3.0 diff --git a/tests/GraphQL.Common.Tests/Request/GraphQLRequestConsts.cs b/tests/GraphQL.Common.Tests/Request/GraphQLRequestConsts.cs index 9d3fca63..520ca4b6 100644 --- a/tests/GraphQL.Common.Tests/Request/GraphQLRequestConsts.cs +++ b/tests/GraphQL.Common.Tests/Request/GraphQLRequestConsts.cs @@ -7,130 +7,122 @@ public static class GraphQLRequestConsts { /// /// /// - public static GraphQLRequest FieldsRequest1 { get; } = new GraphQLRequest { - Query = @" - { - hero { - name - } - }", + public static GraphQLRequest FieldsRequest1 { get; } = new GraphQLRequest(@" + { + hero { + name + } + }") { Variables = null }; /// /// /// - public static GraphQLRequest FieldsRequest2 { get; } = new GraphQLRequest { - Query = @" - { - hero { + public static GraphQLRequest FieldsRequest2 { get; } = new GraphQLRequest(@" + { + hero { + name + # Queries can have comments! + friends { name - # Queries can have comments! - friends { - name - } } - }", + } + }") { Variables = null }; /// /// /// - public static GraphQLRequest ArgumentsRequest1 { get; } = new GraphQLRequest { - Query = @" - { - human(id: ""1000"") { - name - height - } - }", + public static GraphQLRequest ArgumentsRequest1 { get; } = new GraphQLRequest(@" + { + human(id: ""1000"") { + name + height + } + }") { Variables = null }; /// /// /// - public static GraphQLRequest ArgumentsRequest2 { get; } = new GraphQLRequest { - Query = @" - { - human(id: ""1000"") { - name - height(unit: FOOT) - } - }", + public static GraphQLRequest ArgumentsRequest2 { get; } = new GraphQLRequest(@" + { + human(id: ""1000"") { + name + height(unit: FOOT) + } + }") { Variables = null }; /// /// /// - public static GraphQLRequest AliasesRequest { get; } = new GraphQLRequest { - Query = @" - { - empireHero: hero(episode: EMPIRE) { - name - } - jediHero: hero(episode: JEDI) { - name - } - }", + public static GraphQLRequest AliasesRequest { get; } = new GraphQLRequest(@" + { + empireHero: hero(episode: EMPIRE) { + name + } + jediHero: hero(episode: JEDI) { + name + } + }") { Variables = null }; /// /// /// - public static GraphQLRequest FragmentsRequest { get; } = new GraphQLRequest { - Query = @" - { - leftComparison: hero(episode: EMPIRE) { - ...comparisonFields - } - rightComparison: hero(episode: JEDI) { - ...comparisonFields - } + public static GraphQLRequest FragmentsRequest { get; } = new GraphQLRequest(@" + { + leftComparison: hero(episode: EMPIRE) { + ...comparisonFields + } + rightComparison: hero(episode: JEDI) { + ...comparisonFields } + } - fragment comparisonFields on Character { + fragment comparisonFields on Character { + name + appearsIn + friends { name - appearsIn - friends { - name - } - }", + } + }") { Variables = null }; /// /// /// - public static GraphQLRequest OperationNameRequest { get; } = new GraphQLRequest { - Query = @" - query HeroNameAndFriends { - hero { + public static GraphQLRequest OperationNameRequest { get; } = new GraphQLRequest(@" + query HeroNameAndFriends { + hero { + name + friends { name - friends { - name - } } - }", + } + }") { Variables = null }; /// /// /// - public static GraphQLRequest VariablesRequest { get; } = new GraphQLRequest { - Query = @" - query HeroNameAndFriends($episode: Episode) { - hero(episode: $episode) { + public static GraphQLRequest VariablesRequest { get; } = new GraphQLRequest(@" + query HeroNameAndFriends($episode: Episode) { + hero(episode: $episode) { + name + friends { name - friends { - name - } } - }", + } + }") { Variables = new { episode = "JEDI" } @@ -139,16 +131,15 @@ query HeroNameAndFriends($episode: Episode) { /// /// /// - public static GraphQLRequest DirectivesRequest { get; } = new GraphQLRequest { - Query = @" - query Hero($episode: Episode, $withFriends: Boolean!) { - hero(episode: $episode) { + public static GraphQLRequest DirectivesRequest { get; } = new GraphQLRequest(@" + query Hero($episode: Episode, $withFriends: Boolean!) { + hero(episode: $episode) { + name + friends @include(if: $withFriends) { name - friends @include(if: $withFriends) { - name - } } - }", + } + }") { Variables = new { episode = "JEDI", withFriends = false @@ -158,14 +149,13 @@ friends @include(if: $withFriends) { /// /// /// - public static GraphQLRequest MutationsRequest { get; } = new GraphQLRequest { - Query = @" - mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { - createReview(episode: $ep, review: $review) { - stars - commentary - } - }", + public static GraphQLRequest MutationsRequest { get; } = new GraphQLRequest(@" + mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { + createReview(episode: $ep, review: $review) { + stars + commentary + } + }") { Variables = new { ep = "JEDI", review = new { @@ -178,19 +168,18 @@ mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { /// /// /// - public static GraphQLRequest InlineFragmentsRequest { get; } = new GraphQLRequest { - Query = @" - query HeroForEpisode($ep: Episode!) { - hero(episode: $ep) { - name - ... on Droid { - primaryFunction - } - ... on Human { - height - } + public static GraphQLRequest InlineFragmentsRequest { get; } = new GraphQLRequest(@" + query HeroForEpisode($ep: Episode!) { + hero(episode: $ep) { + name + ... on Droid { + primaryFunction } - }", + ... on Human { + height + } + } + }") { Variables = new { ep = "JEDI" } @@ -199,22 +188,21 @@ ... on Human { /// /// /// - public static GraphQLRequest MetaFieldsRequest { get; } = new GraphQLRequest { - Query = @" - { - search(text: ""an"") { - __typename - ...on Human { - name - } - ... on Droid { - name - } - ... on Starship { - name - } + public static GraphQLRequest MetaFieldsRequest { get; } = new GraphQLRequest(@" + { + search(text: ""an"") { + __typename + ...on Human { + name + } + ... on Droid { + name } - }", + ... on Starship { + name + } + } + }") { Variables = null }; diff --git a/tests/GraphQL.Common.Tests/Request/GraphQLRequestTests.cs b/tests/GraphQL.Common.Tests/Request/GraphQLRequestTests.cs index 2512fe3b..6eb0febd 100644 --- a/tests/GraphQL.Common.Tests/Request/GraphQLRequestTests.cs +++ b/tests/GraphQL.Common.Tests/Request/GraphQLRequestTests.cs @@ -7,49 +7,41 @@ public class GraphQLRequestTests { [Fact] public void FieldsRequest1Fact() { var graphQLRequest = GraphQLRequestConsts.FieldsRequest1; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); } [Fact] public void FieldsRequest2Fact() { var graphQLRequest = GraphQLRequestConsts.FieldsRequest2; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); } [Fact] public void ArgumentsRequest1Fact() { var graphQLRequest = GraphQLRequestConsts.ArgumentsRequest1; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); } [Fact] public void ArgumentsRequest2Fact() { var graphQLRequest = GraphQLRequestConsts.ArgumentsRequest2; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); } [Fact] public void AliasesRequestFact() { var graphQLRequest = GraphQLRequestConsts.AliasesRequest; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); } [Fact] public void FragmentsRequestFact() { var graphQLRequest = GraphQLRequestConsts.FragmentsRequest; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); } [Fact] public void OperationNameRequestFact() { var graphQLRequest = GraphQLRequestConsts.OperationNameRequest; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); } [Fact] public void VariablesRequestFact() { var graphQLRequest = GraphQLRequestConsts.VariablesRequest; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); Assert.NotNull(graphQLRequest.Variables); Assert.Equal("JEDI", graphQLRequest.Variables.episode); } @@ -57,7 +49,6 @@ public void VariablesRequestFact() { [Fact] public void DirectivesRequestFact() { var graphQLRequest = GraphQLRequestConsts.DirectivesRequest; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); Assert.NotNull(graphQLRequest.Variables); Assert.Equal("JEDI", graphQLRequest.Variables.episode); Assert.Equal(false, graphQLRequest.Variables.withFriends); @@ -66,7 +57,6 @@ public void DirectivesRequestFact() { [Fact] public void MutationsRequestFact() { var graphQLRequest = GraphQLRequestConsts.MutationsRequest; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); Assert.NotNull(graphQLRequest.Variables); Assert.Equal("JEDI", graphQLRequest.Variables.ep); Assert.Equal(5, graphQLRequest.Variables.review.stars); @@ -76,7 +66,6 @@ public void MutationsRequestFact() { [Fact] public void InlineFragmentsRequestFact() { var graphQLRequest = GraphQLRequestConsts.InlineFragmentsRequest; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); Assert.NotNull(graphQLRequest.Variables); Assert.Equal("JEDI", graphQLRequest.Variables.ep); } @@ -84,7 +73,6 @@ public void InlineFragmentsRequestFact() { [Fact] public void MetaFieldsRequestFact() { var graphQLRequest = GraphQLRequestConsts.MetaFieldsRequest; - AssertGraphQL.CorrectGraphQLRequest(graphQLRequest); } } diff --git a/tests/GraphQL.Integration.Tests/GraphQLClientExtensions.cs b/tests/GraphQL.Integration.Tests/GraphQLClientExtensions.cs index c7dd0568..ff71ee6d 100644 --- a/tests/GraphQL.Integration.Tests/GraphQLClientExtensions.cs +++ b/tests/GraphQL.Integration.Tests/GraphQLClientExtensions.cs @@ -10,14 +10,13 @@ public static class GraphQLClientExtensions { public static async Task AddMessageAsync(this GraphQLHttpClient client, string message) { - var graphQLRequest = new GraphQLRequest - { - Query = @" + var graphQLRequest = new GraphQLRequest(@" mutation($input: MessageInputType){ addMessage(message: $input){ content } - }", + }") + { Variables = new { input = new @@ -30,5 +29,23 @@ public static async Task AddMessageAsync(this GraphQLHttpClient }; return await client.SendMutationAsync(graphQLRequest).ConfigureAwait(false); } + + public static async Task JoinDeveloperUser(this GraphQLHttpClient client) + { + var graphQLRequest = new GraphQLRequest(@" + mutation($userId: String){ + join(userId: $userId){ + displayName + id + } + }") + { + Variables = new + { + userId = "1" + } + }; + return await client.SendMutationAsync(graphQLRequest).ConfigureAwait(false); + } } } diff --git a/tests/GraphQL.Integration.Tests/SubscriptionsTest.cs b/tests/GraphQL.Integration.Tests/SubscriptionsTest.cs index 36804c95..635a0d1c 100644 --- a/tests/GraphQL.Integration.Tests/SubscriptionsTest.cs +++ b/tests/GraphQL.Integration.Tests/SubscriptionsTest.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging; using Xunit; - namespace GraphQL.Integration.Tests { public class SubscriptionsTest @@ -24,7 +23,10 @@ public static IWebHost CreateServer(int port) config["server.urls"] = $"http://localhost:{port}"; var host = new WebHostBuilder() - .ConfigureLogging((ctx, logging) => logging.AddDebug()) + .ConfigureLogging((ctx, logging) => + { + logging.AddDebug(); + }) .UseConfiguration(config) .UseKestrel() .UseStartup() @@ -45,7 +47,7 @@ private GraphQLHttpClient GetGraphQLClient(int port) => new GraphQLHttpClient(new GraphQLHttpClientOptions { EndPoint = new Uri($"http://localhost:{port}/graphql"), - } ); + }); [Fact] @@ -70,10 +72,7 @@ public async void AssertTestingHarness() } }"; - private readonly GraphQLRequest SubscriptionRequest = new GraphQLRequest - { - Query = SubscriptionQuery - }; + private readonly GraphQLRequest SubscriptionRequest = new GraphQLRequest(SubscriptionQuery); [Fact] public async void CanCreateObservableSubscription() @@ -165,21 +164,16 @@ public async void CanReconnectWithSameObservable() private const string SubscriptionQuery2 = @" subscription { - messageAdded{ - content - from { - displayName - } + userJoined{ + displayName + id } }"; - private readonly GraphQLRequest SubscriptionRequest2 = new GraphQLRequest - { - Query = SubscriptionQuery2 - }; + private readonly GraphQLRequest SubscriptionRequest2 = new GraphQLRequest(SubscriptionQuery2); [Fact] - public async void CanConnectMultipleSubscriptionsSimultaneously() + public async void CanConnectTwoSubscriptionsSimultaneously() { var port = NetworkHelpers.GetFreeTcpPortNumber(); var callbackTester = new CallbackTester(); @@ -189,10 +183,12 @@ public async void CanConnectMultipleSubscriptionsSimultaneously() var client = GetGraphQLClient(port); Debug.WriteLine("creating subscription stream"); - IObservable subscription1 = client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Callback); + IObservable observable1 = client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Callback); + IObservable observable2 = client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Callback); Debug.WriteLine("subscribing..."); - var tester = subscription1.SubscribeTester(); + var tester = observable1.SubscribeTester(); + var tester2 = observable2.SubscribeTester(); const string message1 = "Hello World"; var response = await client.AddMessageAsync(message1).ConfigureAwait(false); @@ -202,42 +198,31 @@ public async void CanConnectMultipleSubscriptionsSimultaneously() Assert.Equal(message1, (string)gqlResponse.Data.messageAdded.content.Value); }); - IObservable subscription2 = client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Callback); - var tester2 = subscription2.SubscribeTester(); - tester2.ShouldHaveReceivedUpdate(gqlResponse => - { - Assert.Equal(message1, (string)gqlResponse.Data.messageAdded.content.Value); - Assert.Equal("tester", (string)gqlResponse.Data.messageAdded.from.displayName.Value); - }, TimeSpan.FromSeconds(10)); + await Task.Delay(500); // ToDo: can be removed after https://github.com/graphql-dotnet/server/pull/199 was merged and released + + response = await client.JoinDeveloperUser().ConfigureAwait(false); + Assert.Equal("developer", (string)response.Data.join.displayName.Value); - const string message2 = "How are you?"; - response = await client.AddMessageAsync(message2).ConfigureAwait(false); - Assert.Equal(message2, (string)response.Data.addMessage.content); - tester.ShouldHaveReceivedUpdate(gqlResponse => - { - Assert.Equal(message2, (string)gqlResponse.Data.messageAdded.content.Value); - }); tester2.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal(message2, (string)gqlResponse.Data.messageAdded.content.Value); - Assert.Equal("tester", (string)gqlResponse.Data.messageAdded.from.displayName.Value); + Assert.Equal("1", (string)gqlResponse.Data.userJoined.id.Value); + Assert.Equal("developer", (string)gqlResponse.Data.userJoined.displayName.Value); }); - + Debug.WriteLine("disposing subscription..."); - tester.Dispose(); + tester2.Dispose(); const string message3 = "lorem ipsum dolor si amet"; response = await client.AddMessageAsync(message3).ConfigureAwait(false); Assert.Equal(message3, (string)response.Data.addMessage.content); - tester2.ShouldHaveReceivedUpdate(gqlResponse => + tester.ShouldHaveReceivedUpdate(gqlResponse => { Assert.Equal(message3, (string)gqlResponse.Data.messageAdded.content.Value); - Assert.Equal("tester", (string)gqlResponse.Data.messageAdded.from.displayName.Value); }); // disposing the client should complete the subscription client.Dispose(); - tester2.ShouldHaveCompleted(); + tester.ShouldHaveCompleted(); } } diff --git a/tests/IntegrationTestServer/ChatSchema/ChatMutation.cs b/tests/IntegrationTestServer/ChatSchema/ChatMutation.cs index 7c2a2333..0ca1dfbd 100644 --- a/tests/IntegrationTestServer/ChatSchema/ChatMutation.cs +++ b/tests/IntegrationTestServer/ChatSchema/ChatMutation.cs @@ -16,7 +16,18 @@ public ChatMutation(IChat chat) var message = chat.AddMessage(receivedMessage); return message; }); - } + + Field("join", + arguments: new QueryArguments( + new QueryArgument { Name = "userId" } + ), + resolve: context => + { + var userId = context.GetArgument("userId"); + var userJoined = chat.Join(userId); + return userJoined; + }); + } } public class MessageInputType : InputObjectGraphType diff --git a/tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs b/tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs index 3c8c9624..0ceb77a5 100644 --- a/tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs +++ b/tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs @@ -25,7 +25,15 @@ public ChatSubscriptions(IChat chat) Subscriber = new EventStreamResolver(Subscribe) }); - AddField(new EventStreamFieldType + AddField(new EventStreamFieldType + { + Name = "contentAdded", + Type = typeof(MessageType), + Resolver = new FuncFieldResolver(ResolveMessage), + Subscriber = new EventStreamResolver(Subscribe) + }); + + AddField(new EventStreamFieldType { Name = "messageAddedByUser", Arguments = new QueryArguments( @@ -35,7 +43,15 @@ public ChatSubscriptions(IChat chat) Resolver = new FuncFieldResolver(ResolveMessage), Subscriber = new EventStreamResolver(SubscribeById) }); - } + + AddField(new EventStreamFieldType + { + Name = "userJoined", + Type = typeof(MessageFromType), + Resolver = new FuncFieldResolver(context => context.Source as MessageFrom), + Subscriber = new EventStreamResolver(context => _chat.UserJoined()) + }); + } private IObservable SubscribeById(ResolveEventStreamContext context) { diff --git a/tests/IntegrationTestServer/ChatSchema/IChat.cs b/tests/IntegrationTestServer/ChatSchema/IChat.cs index 68c8e86b..e52c0432 100644 --- a/tests/IntegrationTestServer/ChatSchema/IChat.cs +++ b/tests/IntegrationTestServer/ChatSchema/IChat.cs @@ -11,16 +11,20 @@ public interface IChat Message AddMessage(Message message); - IObservable Messages(string user); + MessageFrom Join(string userId); - Message AddMessage(ReceivedMessage message); + IObservable Messages(string user); + IObservable UserJoined(); + + Message AddMessage(ReceivedMessage message); } public class Chat : IChat { private readonly ISubject _messageStream = new ReplaySubject(1); + private readonly ISubject _userJoined = new Subject(); - public Chat() + public Chat() { AllMessages = new ConcurrentStack(); Users = new ConcurrentDictionary @@ -58,9 +62,25 @@ public Message AddMessage(Message message) AllMessages.Push(message); _messageStream.OnNext(message); return message; - } + } + + public MessageFrom Join(string userId) + { + if (!Users.TryGetValue(userId, out var displayName)) + { + displayName = "(unknown)"; + } + + var joinedUser = new MessageFrom { + Id = userId, + DisplayName = displayName + }; - public IObservable Messages(string user) + _userJoined.OnNext(joinedUser); + return joinedUser; + } + + public IObservable Messages(string user) { return _messageStream .Select(message => @@ -75,5 +95,16 @@ public void AddError(Exception exception) { _messageStream.OnError(exception); } - } + + public IObservable UserJoined() + { + return _userJoined.AsObservable(); + } + } + + public class User + { + public string Id { get; set; } + public string Name { get; set; } + } } diff --git a/tests/IntegrationTestServer/IntegrationTestServer.csproj b/tests/IntegrationTestServer/IntegrationTestServer.csproj index 179554ff..cfba48cd 100644 --- a/tests/IntegrationTestServer/IntegrationTestServer.csproj +++ b/tests/IntegrationTestServer/IntegrationTestServer.csproj @@ -9,10 +9,12 @@ - - - - + + + + + + diff --git a/tests/IntegrationTestServer/Program.cs b/tests/IntegrationTestServer/Program.cs index fd8448a8..9b0392b7 100644 --- a/tests/IntegrationTestServer/Program.cs +++ b/tests/IntegrationTestServer/Program.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; namespace IntegrationTestServer { @@ -12,6 +13,7 @@ public static void Main(string[] args) public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) - .UseStartup(); + .UseStartup() + .ConfigureLogging((ctx, logging) => logging.SetMinimumLevel(LogLevel.Debug)); } } diff --git a/tests/IntegrationTestServer/Startup.cs b/tests/IntegrationTestServer/Startup.cs index 0161c739..166bf202 100644 --- a/tests/IntegrationTestServer/Startup.cs +++ b/tests/IntegrationTestServer/Startup.cs @@ -1,6 +1,7 @@ using GraphQL.Server; using GraphQL.Server.Ui.GraphiQL; using GraphQL.Server.Ui.Voyager; +using GraphQL.Server.Ui.Playground; using IntegrationTestServer.ChatSchema; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -63,6 +64,10 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) GraphQLEndPoint = "/graphql", Path = "/ui/voyager" }); + app.UseGraphQLPlayground(new GraphQLPlaygroundOptions + { + Path = "/ui/playground" + }); app.UseMvc(); } }