From 292e7c6edecb9032c6ceb3f00345ed84fc6051e9 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 24 Feb 2020 08:56:12 +0100 Subject: [PATCH 01/24] make README example consistent with example project, fix default serializer --- GraphQL.Client.sln | 21 ++--- README.md | 92 ++++++++++--------- .../GraphQL.Client.Example.csproj} | 5 - .../PersonAndFilmsResponse.cs | 21 +++++ examples/GraphQL.Client.Example/Program.cs | 49 ++++++++++ .../GraphQL.Client.Http.Examples/Program.cs | 41 --------- src/GraphQL.Client/GraphQLHttpClient.cs | 8 +- 7 files changed, 132 insertions(+), 105 deletions(-) rename examples/{GraphQL.Client.Http.Examples/GraphQL.Client.Http.Examples.csproj => GraphQL.Client.Example/GraphQL.Client.Example.csproj} (60%) create mode 100644 examples/GraphQL.Client.Example/PersonAndFilmsResponse.cs create mode 100644 examples/GraphQL.Client.Example/Program.cs delete mode 100644 examples/GraphQL.Client.Http.Examples/Program.cs diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln index e1943dde..24ea23a4 100644 --- a/GraphQL.Client.sln +++ b/GraphQL.Client.sln @@ -31,8 +31,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{6326E0 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Server.Test", "tests\GraphQL.Server.Test\GraphQL.Server.Test.csproj", "{E95A1258-F666-4D4E-9101-E0C46F6A3CB3}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{C42106CF-F685-4F29-BC18-A70616BD68A0}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{05CAF9B2-981E-40C0-AE31-5FA56E351F12}" ProjectSection(SolutionItems) = preProject .github\workflows\branches-ubuntu.yml = .github\workflows\branches-ubuntu.yml @@ -40,10 +38,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\main.yml = .github\workflows\main.yml EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{D61415CA-D822-43DD-9AE7-993B8B60E855}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Http.Examples", "examples\GraphQL.Client.Http.Examples\GraphQL.Client.Http.Examples.csproj", "{95D78D57-3232-491D-BAD6-F373D76EA34D}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Primitives", "src\GraphQL.Primitives\GraphQL.Primitives.csproj", "{87FC440E-6A4D-47D8-9EB2-416FC31CC4A6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Primitives.Tests", "tests\GraphQL.Primitives.Tests\GraphQL.Primitives.Tests.csproj", "{C212983F-67DB-44EB-BFB0-5DA75A86DF55}" @@ -68,6 +62,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Tests.Common EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Serializer.SystemTextJson", "src\GraphQL.Client.Serializer.SystemTextJson\GraphQL.Client.Serializer.SystemTextJson.csproj", "{7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{89AD33AB-64F6-4F82-822F-21DF7A10CEC0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.Example", "examples\GraphQL.Client.Example\GraphQL.Client.Example.csproj", "{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -78,10 +76,6 @@ Global {E95A1258-F666-4D4E-9101-E0C46F6A3CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU {E95A1258-F666-4D4E-9101-E0C46F6A3CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU {E95A1258-F666-4D4E-9101-E0C46F6A3CB3}.Release|Any CPU.Build.0 = Release|Any CPU - {95D78D57-3232-491D-BAD6-F373D76EA34D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {95D78D57-3232-491D-BAD6-F373D76EA34D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {95D78D57-3232-491D-BAD6-F373D76EA34D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {95D78D57-3232-491D-BAD6-F373D76EA34D}.Release|Any CPU.Build.0 = Release|Any CPU {87FC440E-6A4D-47D8-9EB2-416FC31CC4A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {87FC440E-6A4D-47D8-9EB2-416FC31CC4A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {87FC440E-6A4D-47D8-9EB2-416FC31CC4A6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -130,14 +124,16 @@ Global {7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5}.Debug|Any CPU.Build.0 = Debug|Any CPU {7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5}.Release|Any CPU.Build.0 = Release|Any CPU + {6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {E95A1258-F666-4D4E-9101-E0C46F6A3CB3} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} - {05CAF9B2-981E-40C0-AE31-5FA56E351F12} = {C42106CF-F685-4F29-BC18-A70616BD68A0} - {95D78D57-3232-491D-BAD6-F373D76EA34D} = {D61415CA-D822-43DD-9AE7-993B8B60E855} {87FC440E-6A4D-47D8-9EB2-416FC31CC4A6} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} {C212983F-67DB-44EB-BFB0-5DA75A86DF55} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} {92107DF5-73DF-4371-8EB1-6734FED704AD} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} @@ -150,6 +146,7 @@ Global {CA842D18-FC4A-4281-B1FF-080FA91887B8} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} {0D307BAD-27AE-4A5D-8764-4AA2620B01E9} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} {7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} + {6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD} = {89AD33AB-64F6-4F82-822F-21DF7A10CEC0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4} diff --git a/README.md b/README.md index 95d46939..02f6dee7 100644 --- a/README.md +++ b/README.md @@ -16,66 +16,76 @@ The Library will try to follow the following standards and documents: ```csharp var heroRequest = new GraphQLRequest { Query = @" - { - hero { - name - } - }" + { + hero { + name + } + }" }; ``` #### OperationName and Variables Request: + ```csharp -var heroAndFriendsRequest = new GraphQLRequest { +var personAndFilmsRequest = new GraphQLRequest { Query =@" - query HeroNameAndFriends($episode: Episode) { - hero(episode: $episode) { - name - friends { - name - } - } - }", - OperationName = "HeroNameAndFriends", - Variables = new { - episode = "JEDI" - } + query PersonAndFilms($id: ID) { + person(id: $id) { + name + filmConnection { + films { + title + } + } + } + }", + OperationName = "PersonAndFilms", + Variables = new { + id = "cGVvcGxlOjE=" + } }; ``` Be careful when using `byte[]` in your variables object, as most JSON serializers will treat that as binary data! If you really need to send a *list of bytes* with a `byte[]` as a source, then convert it to a `List` first, which will tell the serializer to output a list of numbers instead of a base64-encoded string. ### Execute Query/Mutation: + ```csharp var graphQLClient = new GraphQLHttpClient("https://swapi.apis.guru/"); -public class HeroAndFriendsResponse { - public Hero Hero {get; set;} +public class PersonAndFilmsResponse { + public PersonContent Person { get; set; } - public class Hero { - public string Name {get; set;} + public class PersonContent { + public string Name { get; set; } + public FilmConnectionContent FilmConnection { get; set; } - public List Friends {get; set;} + public class FilmConnectionContent { + public List Films { get; set; } + + public class FilmContent { + public string Title { get; set; } + } + } } } -var graphQLResponse = await graphQLClient.SendQueryAsync(heroAndFriendsRequest); +var graphQLResponse = await graphQLClient.SendQueryAsync(personAndFilmsRequest); -var heroName = graphQLResponse.Data.Hero.Name; +var personName = graphQLResponse.Data.Person.Name; ``` - ### Use Subscriptions ```csharp public class UserJoinedSubscriptionResult { - public ChatUser UserJoined { get; set; } + public ChatUser UserJoined { get; set; } - public class ChatUser { - public string DisplayName { get; set; } - public string Id { get; set; } - } + public class ChatUser { + public string DisplayName { get; set; } + public string Id { get; set; } + } } ``` @@ -84,21 +94,21 @@ public class UserJoinedSubscriptionResult { ```csharp var userJoinedRequest = new GraphQLRequest { Query = @" - subscription { - userJoined{ - displayName - id - } - }" + subscription { + userJoined{ + displayName + id + } + }" }; IObservable> subscriptionStream - = client.CreateSubscriptionStream(userJoinedRequest); + = client.CreateSubscriptionStream(userJoinedRequest); var subscription = subscriptionStream.Subscribe(response => - { - Console.WriteLine($"user '{response.Data.UserJoined.DisplayName}' joined") - }); + { + Console.WriteLine($"user '{response.Data.UserJoined.DisplayName}' joined") + }); ``` #### End Subscription diff --git a/examples/GraphQL.Client.Http.Examples/GraphQL.Client.Http.Examples.csproj b/examples/GraphQL.Client.Example/GraphQL.Client.Example.csproj similarity index 60% rename from examples/GraphQL.Client.Http.Examples/GraphQL.Client.Http.Examples.csproj rename to examples/GraphQL.Client.Example/GraphQL.Client.Example.csproj index 2f2dea3c..d9680f86 100644 --- a/examples/GraphQL.Client.Http.Examples/GraphQL.Client.Http.Examples.csproj +++ b/examples/GraphQL.Client.Example/GraphQL.Client.Example.csproj @@ -6,13 +6,8 @@ false - - - - - diff --git a/examples/GraphQL.Client.Example/PersonAndFilmsResponse.cs b/examples/GraphQL.Client.Example/PersonAndFilmsResponse.cs new file mode 100644 index 00000000..a9ef5a9b --- /dev/null +++ b/examples/GraphQL.Client.Example/PersonAndFilmsResponse.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace GraphQL.Client.Http.Examples +{ + public class PersonAndFilmsResponse { + public PersonContent Person { get; set; } + + public class PersonContent { + public string Name { get; set; } + public FilmConnectionContent FilmConnection { get; set; } + + public class FilmConnectionContent { + public List Films { get; set; } + + public class FilmContent { + public string Title { get; set; } + } + } + } + } +} diff --git a/examples/GraphQL.Client.Example/Program.cs b/examples/GraphQL.Client.Example/Program.cs new file mode 100644 index 00000000..41443afd --- /dev/null +++ b/examples/GraphQL.Client.Example/Program.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using GraphQL.Client.Serializer.Newtonsoft; + +namespace GraphQL.Client.Http.Examples { + + public class Program { + + public static async Task Main(string[] args) { + + var serializer = new NewtonsoftJsonSerializer(); + using var graphQLClient = new GraphQLHttpClient("https://swapi.apis.guru/"); + + var personAndFilmsRequest = new GraphQLRequest { + Query = @" + query PersonAndFilms($id: ID) { + person(id: $id) { + name + filmConnection { + films { + title + } + } + } + }", + OperationName = "PersonAndFilms", + Variables = new { + id = "cGVvcGxlOjE=" + } + }; + + var graphQLResponse = await graphQLClient.SendQueryAsync(personAndFilmsRequest); + Console.WriteLine("raw response:"); + Console.WriteLine(JsonSerializer.Serialize(graphQLResponse, new JsonSerializerOptions { WriteIndented = true })); + + Console.WriteLine(); + Console.WriteLine($"Name: {graphQLResponse.Data.Person.Name}" ); + var films = string.Join(", ", graphQLResponse.Data.Person.FilmConnection.Films.Select(f => f.Title)); + Console.WriteLine($"Films: {films}"); + + Console.WriteLine(); + Console.WriteLine("Press any key to quit..."); + Console.ReadKey(); + } + + } +} diff --git a/examples/GraphQL.Client.Http.Examples/Program.cs b/examples/GraphQL.Client.Http.Examples/Program.cs deleted file mode 100644 index 0cce0283..00000000 --- a/examples/GraphQL.Client.Http.Examples/Program.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using GraphQL.Server.Test.GraphQL.Models; -using Microsoft.AspNetCore.TestHost; - -namespace GraphQL.Client.Http.Examples { - - public class Program { - - private static readonly TestServer testServer = new TestServer(Server.Test.Program.CreateHostBuilder()) { - AllowSynchronousIO = true - }; - - public static async Task Main(string[] args) { - using var httpClient = testServer.CreateClient(); - using var graphqlClient = httpClient.AsGraphQLClient($"{testServer.BaseAddress}graphql"); - var graphQLRequest = new GraphQLRequest( - @" - { - repository(owner: ""graphql-dotnet"", name: ""graphql-client"") { - databaseId, - id, - name, - url - } - }" - ); - var graphQLResponse = await graphqlClient.SendQueryAsync(graphQLRequest); - Console.WriteLine(JsonSerializer.Serialize(graphQLResponse, new JsonSerializerOptions { WriteIndented = true })); - } - - private class Schema { - - public Repository Repository { get; set; } - - } - - } - -} diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index dbf4bcb7..eb25ebc0 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -7,6 +7,7 @@ using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http.Websocket; +using GraphQL.Client.Serializer.Newtonsoft; namespace GraphQL.Client.Http { @@ -48,12 +49,7 @@ public GraphQLHttpClient(Action configure) : this(conf public GraphQLHttpClient(GraphQLHttpClientOptions options) : this(options, new HttpClient(options.HttpMessageHandler)) { } - public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient) { - Options = options; - this.HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), this); - Options.JsonSerializer = JsonSerializer.EnsureAssigned(); - } + public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient) : this(options, httpClient, new NewtonsoftJsonSerializer()) { } public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient, IGraphQLWebsocketJsonSerializer serializer) { Options = options ?? throw new ArgumentNullException(nameof(options)); From fec61b3d53366b5148b14036bdfaa1141f3b2b8a Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 24 Feb 2020 10:24:48 +0100 Subject: [PATCH 02/24] remove unnecessary code in example --- examples/GraphQL.Client.Example/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/GraphQL.Client.Example/Program.cs b/examples/GraphQL.Client.Example/Program.cs index 41443afd..a7cc00fe 100644 --- a/examples/GraphQL.Client.Example/Program.cs +++ b/examples/GraphQL.Client.Example/Program.cs @@ -10,7 +10,6 @@ public class Program { public static async Task Main(string[] args) { - var serializer = new NewtonsoftJsonSerializer(); using var graphQLClient = new GraphQLHttpClient("https://swapi.apis.guru/"); var personAndFilmsRequest = new GraphQLRequest { From 049ed1a856ddc307f353e85a53297b0d45f588b1 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 24 Feb 2020 11:09:20 +0100 Subject: [PATCH 03/24] create tests to cover request cancellation ( #140) --- .../Chat/Schema/ChatQuery.cs | 9 ++++++++ .../QueryAndMutationTests/Base.cs | 21 +++++++++++++++++++ .../WebsocketTests/Base.cs | 19 ++++++++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs index 32357338..641ca262 100644 --- a/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using GraphQL.Types; namespace GraphQL.Client.Tests.Common.Chat.Schema { @@ -21,6 +23,13 @@ public ChatQuery(IChat chat) { context.Errors.Add(new ExecutionError("this error contains extension fields", TestExtensions)); return null; }); + + Field() + .Name("longRunning") + .ResolveAsync(async context => { + await Task.Delay(TimeSpan.FromSeconds(5)); + return "finally returned"; + }); } } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 16e73c92..39380ea4 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -1,4 +1,8 @@ +using System; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; @@ -155,5 +159,22 @@ public async void PreprocessHttpRequestMessageIsCalled() { Assert.Equal("Luke", response.Data.Human.Name); } } + + [Fact] + public void PostRequestCanBeCancelled() { + var graphQLRequest = new GraphQLRequest(@" + query Long { + longRunning + }"); + + using (var setup = WebHostHelpers.SetupTest(false, serializer)) { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + + Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new {longRunning = string.Empty}, cts.Token); + Action timeMeasurement = () => requestTask.Should().Throw(); + + timeMeasurement.ExecutionTime().Should().BeCloseTo(TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(200)); + } + } } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 6112a00c..1e4d0903 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Net.WebSockets; using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; @@ -38,7 +39,6 @@ public async void AssertTestingHarness() { } } - [Fact] public async void CanSendRequestViaWebsocket() { var port = NetworkHelpers.GetFreeTcpPortNumber(); @@ -51,6 +51,23 @@ public async void CanSendRequestViaWebsocket() { } } + [Fact] + public void PostRequestCanBeCancelled() { + var graphQLRequest = new GraphQLRequest(@" + query Long { + longRunning + }"); + + using (var setup = WebHostHelpers.SetupTest(true, Serializer)) { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + + Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token); + Action timeMeasurement = () => requestTask.Should().Throw(); + + timeMeasurement.ExecutionTime().Should().BeCloseTo(TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(200)); + } + } + [Fact] public async void CanHandleRequestErrorViaWebsocket() { var port = NetworkHelpers.GetFreeTcpPortNumber(); From af1024720889f293c8a45c32b35e3c6aaea81555 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 24 Feb 2020 11:28:52 +0100 Subject: [PATCH 04/24] speed up cancellation tests --- tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs | 2 +- tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 39380ea4..18a0e43f 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -168,7 +168,7 @@ query Long { }"); using (var setup = WebHostHelpers.SetupTest(false, serializer)) { - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new {longRunning = string.Empty}, cts.Token); Action timeMeasurement = () => requestTask.Should().Throw(); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 1e4d0903..b3aaf95f 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -52,14 +52,14 @@ public async void CanSendRequestViaWebsocket() { } [Fact] - public void PostRequestCanBeCancelled() { + public void WebsocketRequestCanBeCancelled() { var graphQLRequest = new GraphQLRequest(@" query Long { longRunning }"); using (var setup = WebHostHelpers.SetupTest(true, Serializer)) { - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token); Action timeMeasurement = () => requestTask.Should().Throw(); From a0e9a16fdb7b1513ccb69e80b74289338e8a053c Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 24 Feb 2020 12:21:40 +0100 Subject: [PATCH 05/24] fix expected execution time --- .../QueryAndMutationTests/Base.cs | 5 +++-- tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 18a0e43f..304e2982 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -168,12 +168,13 @@ query Long { }"); using (var setup = WebHostHelpers.SetupTest(false, serializer)) { - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + var cancellationTimeout = TimeSpan.FromSeconds(1); + var cts = new CancellationTokenSource(cancellationTimeout); Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new {longRunning = string.Empty}, cts.Token); Action timeMeasurement = () => requestTask.Should().Throw(); - timeMeasurement.ExecutionTime().Should().BeCloseTo(TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(200)); + timeMeasurement.ExecutionTime().Should().BeCloseTo(cancellationTimeout, TimeSpan.FromMilliseconds(50)); } } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index b3aaf95f..77045a92 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -59,12 +59,13 @@ query Long { }"); using (var setup = WebHostHelpers.SetupTest(true, Serializer)) { - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + var cancellationTimeout = TimeSpan.FromSeconds(1); + var cts = new CancellationTokenSource(cancellationTimeout); - Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token); + Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new {longRunning = string.Empty}, cts.Token); Action timeMeasurement = () => requestTask.Should().Throw(); - timeMeasurement.ExecutionTime().Should().BeCloseTo(TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(200)); + timeMeasurement.ExecutionTime().Should().BeCloseTo(cancellationTimeout, TimeSpan.FromMilliseconds(50)); } } From 62afa73705e49f2c5bc55e27691f1ae010024ea4 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 25 Feb 2020 15:41:26 +0100 Subject: [PATCH 06/24] fix cancellation test --- .../Chat/Schema/ChatQuery.cs | 14 +++-- .../Helpers/ConcurrentTaskWrapper.cs | 54 +++++++++++++++++++ .../GraphQL.Integration.Tests.csproj | 1 + .../WebsocketTests/Base.cs | 48 +++++++++++++---- 4 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 tests/GraphQL.Client.Tests.Common/Helpers/ConcurrentTaskWrapper.cs diff --git a/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs index 641ca262..0816fd8c 100644 --- a/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; +using System.Threading; using GraphQL.Types; namespace GraphQL.Client.Tests.Common.Chat.Schema { @@ -12,6 +12,12 @@ public class ChatQuery : ObjectGraphType { {"another extension", 4711} }; + // properties for unit testing + + public readonly ManualResetEventSlim LongRunningQueryBlocker = new ManualResetEventSlim(); + public readonly ManualResetEventSlim WaitingOnQueryBlocker = new ManualResetEventSlim(); + + public ChatQuery(IChat chat) { Name = "ChatQuery"; @@ -26,8 +32,10 @@ public ChatQuery(IChat chat) { Field() .Name("longRunning") - .ResolveAsync(async context => { - await Task.Delay(TimeSpan.FromSeconds(5)); + .Resolve(context => { + WaitingOnQueryBlocker.Set(); + LongRunningQueryBlocker.Wait(); + WaitingOnQueryBlocker.Reset(); return "finally returned"; }); } diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ConcurrentTaskWrapper.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ConcurrentTaskWrapper.cs new file mode 100644 index 00000000..64ea0554 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ConcurrentTaskWrapper.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; + +namespace GraphQL.Client.Tests.Common.Helpers { + + public class ConcurrentTaskWrapper { + public static ConcurrentTaskWrapper New(Func> createTask) { + return new ConcurrentTaskWrapper(createTask); + } + + private readonly Func createTask; + private Task internalTask = null; + + public ConcurrentTaskWrapper(Func createTask) { + this.createTask = createTask; + } + + public Task Invoke() { + if (internalTask != null) + return internalTask; + + return internalTask = createTask(); + } + } + + public class ConcurrentTaskWrapper { + private readonly Func> createTask; + private Task internalTask = null; + + public ConcurrentTaskWrapper(Func> createTask) { + this.createTask = createTask; + } + + public Task Invoke() { + if (internalTask != null) + return internalTask; + + return internalTask = createTask(); + } + + public void Start() { + if (internalTask == null) + internalTask = createTask(); + } + + public Func> Invoking() { + return Invoke; + } + + public void Clear() { + internalTask = null; + } + } +} diff --git a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj index 3273adae..e2e2d726 100644 --- a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj +++ b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 77045a92..0e1cfa6b 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -2,16 +2,22 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net.WebSockets; +using System.Reactive.Concurrency; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using FluentAssertions.Extensions; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Tests.Common.Chat; +using GraphQL.Client.Tests.Common.Chat.Schema; using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; using IntegrationTestServer; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Reactive.Testing; using Xunit; using Xunit.Abstractions; @@ -52,23 +58,47 @@ public async void CanSendRequestViaWebsocket() { } [Fact] - public void WebsocketRequestCanBeCancelled() { + public async void WebsocketRequestCanBeCancelled() { var graphQLRequest = new GraphQLRequest(@" query Long { longRunning }"); using (var setup = WebHostHelpers.SetupTest(true, Serializer)) { - var cancellationTimeout = TimeSpan.FromSeconds(1); - var cts = new CancellationTokenSource(cancellationTimeout); - - Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new {longRunning = string.Empty}, cts.Token); - Action timeMeasurement = () => requestTask.Should().Throw(); - - timeMeasurement.ExecutionTime().Should().BeCloseTo(cancellationTimeout, TimeSpan.FromMilliseconds(50)); + await setup.Client.InitializeWebsocketConnection(); + var chatQuery = setup.Server.Services.GetService(); + var cts = new CancellationTokenSource(); + + var request = + ConcurrentTaskWrapper.New(() => setup.Client.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token)); + + // Test regular request + // start request + request.Start(); + // wait until the query has reached the server + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + // unblock the query + chatQuery.LongRunningQueryBlocker.Set(); + // check execution time + request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); + + // reset stuff + chatQuery.LongRunningQueryBlocker.Reset(); + request.Clear(); + + // cancellation test + request.Start(); + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + cts.Cancel(); + FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) + .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + + // let the server finish its query + chatQuery.LongRunningQueryBlocker.Set(); } } - + [Fact] public async void CanHandleRequestErrorViaWebsocket() { var port = NetworkHelpers.GetFreeTcpPortNumber(); From 53761b1fee8f03e64240373ce277f10d9090e45b Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 25 Feb 2020 19:03:09 +0100 Subject: [PATCH 07/24] use class fixtures, fix disposal of GraphQLHttpWebSocket --- src/GraphQL.Client/GraphQLHttpClient.cs | 4 +- .../Websocket/GraphQLHttpWebSocket.cs | 30 +- .../Websocket/GraphQLHttpWebsocketHelpers.cs | 10 +- .../Chat/Schema/IChat.cs | 50 +- tests/GraphQL.Client.Tests.Common/Common.cs | 12 +- .../Helpers/IntegrationServerTestFixture.cs | 58 +++ .../Helpers/WebHostHelpers.cs | 44 +- .../QueryAndMutationTests/Base.cs | 158 +++--- .../QueryAndMutationTests/Newtonsoft.cs | 6 +- .../QueryAndMutationTests/SystemTextJson.cs | 6 +- .../WebsocketTests/Base.cs | 448 ++++++++---------- .../WebsocketTests/Newtonsoft.cs | 9 +- .../WebsocketTests/SystemTextJson.cs | 7 +- tests/IntegrationTestServer/Program.cs | 2 +- tests/IntegrationTestServer/Startup.cs | 35 +- tests/IntegrationTestServer/StartupChat.cs | 22 - .../IntegrationTestServer/StartupStarWars.cs | 22 - 17 files changed, 496 insertions(+), 427 deletions(-) create mode 100644 tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs delete mode 100644 tests/IntegrationTestServer/StartupChat.cs delete mode 100644 tests/IntegrationTestServer/StartupStarWars.cs diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index eb25ebc0..a4da13c6 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Net.Http; using System.Text; using System.Threading; @@ -164,9 +165,10 @@ public void Dispose() { private void _dispose() { disposed = true; + Debug.WriteLine($"disposing GraphQLHttpClient on endpoint {Options.EndPoint}"); + cancellationTokenSource.Cancel(); this.HttpClient.Dispose(); this.graphQlHttpWebSocket.Dispose(); - cancellationTokenSource.Cancel(); cancellationTokenSource.Dispose(); } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 832fb4cf..1f824c8e 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -17,6 +17,7 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly GraphQLHttpClient client; private readonly ArraySegment buffer; private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationToken cancellationToken; private readonly Subject requestSubject = new Subject(); private readonly Subject exceptionSubject = new Subject(); private readonly BehaviorSubject stateSubject = @@ -41,6 +42,7 @@ internal class GraphQLHttpWebSocket : IDisposable { public IObservable ResponseStream { get; } public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { + cancellationToken = cancellationTokenSource.Token; this.webSocketUri = webSocketUri; this.client = client; buffer = new ArraySegment(new byte[8192]); @@ -59,7 +61,7 @@ public Task SendWebSocketRequest(GraphQLWebSocketRequest request) { private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request) { try { - if (cancellationTokenSource.Token.IsCancellationRequested) { + if (cancellationToken.IsCancellationRequested) { request.SendCanceled(); return; } @@ -70,7 +72,7 @@ await this.clientWebSocket.SendAsync( new ArraySegment(requestBytes), WebSocketMessageType.Text, true, - cancellationTokenSource.Token).ConfigureAwait(false); + cancellationToken).ConfigureAwait(false); request.SendCompleted(); } catch (Exception e) { @@ -126,7 +128,7 @@ public Task InitializeWebSocket() { clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; #endif - return initializeWebSocketTask = _connectAsync(cancellationTokenSource.Token); + return initializeWebSocketTask = _connectAsync(cancellationToken); } } @@ -172,6 +174,8 @@ private IObservable _createResponseStream() { } private async Task _createResultStream(IObserver observer, CancellationToken token) { + cancellationToken.ThrowIfCancellationRequested(); + if (responseSubject == null || responseSubject.IsDisposed) { // create new response subject responseSubject = new Subject(); @@ -200,7 +204,7 @@ private async Task _createResultStream(IObserver { - Debug.WriteLine("response stream disposed"); + Debug.WriteLine($"response stream {responseSubject.GetHashCode()} disposed"); }) ); } @@ -213,6 +217,7 @@ private async Task _createResultStream(IObserver private Task _getReceiveTask() { lock (receiveTaskLocker) { + cancellationToken.ThrowIfCancellationRequested(); if (receiveAsyncTask == null || receiveAsyncTask.IsFaulted || receiveAsyncTask.IsCompleted) @@ -233,13 +238,13 @@ private async Task _receiveResultAsync() { using (var ms = new MemoryStream()) { WebSocketReceiveResult webSocketReceiveResult = null; do { - cancellationTokenSource.Token.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); } while (!webSocketReceiveResult.EndOfMessage); - cancellationTokenSource.Token.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); ms.Seek(0, SeekOrigin.Begin); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { @@ -258,7 +263,7 @@ private async Task _receiveResultAsync() { } } - private async Task _closeAsync(CancellationToken cancellationToken = default) { + private async Task _closeAsync() { if (clientWebSocket == null) return; @@ -271,7 +276,7 @@ private async Task _closeAsync(CancellationToken cancellationToken = default) { } Debug.WriteLine($"closing websocket {clientWebSocket.GetHashCode()}"); - await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cancellationToken).ConfigureAwait(false); + await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); } @@ -301,6 +306,15 @@ private async Task CompleteAsync() { await _closeAsync().ConfigureAwait(false); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); + + responseSubject?.OnCompleted(); + responseSubject?.Dispose(); + + stateSubject?.OnCompleted(); + stateSubject?.Dispose(); + + exceptionSubject?.OnCompleted(); + exceptionSubject?.Dispose(); cancellationTokenSource.Dispose(); Debug.WriteLine($"websocket {clientWebSocket.GetHashCode()} disposed"); } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index 9148a727..f4eeebd5 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -58,8 +58,14 @@ internal static IObservable> CreateSubscriptionStream o.OnCompleted(); } }, - o.OnError, - o.OnCompleted) + e => { + Debug.WriteLine($"response stream for subscription {startRequest.Id} failed: {e}"); + o.OnError(e); + }, + () => { + Debug.WriteLine($"response stream for subscription {startRequest.Id} completed"); + o.OnCompleted(); + }) ); try { diff --git a/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs index 2e31e8b4..6d53e42b 100644 --- a/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs @@ -18,20 +18,25 @@ public interface IChat { } public class Chat : IChat { - private readonly ISubject _messageStream = new ReplaySubject(1); + private readonly RollingReplaySubject _messageStream = new RollingReplaySubject(); private readonly ISubject _userJoined = new Subject(); public Chat() { + Reset(); + } + + public void Reset() { AllMessages = new ConcurrentStack(); Users = new ConcurrentDictionary { ["1"] = "developer", ["2"] = "tester" }; + _messageStream.Clear(); } - public ConcurrentDictionary Users { get; set; } + public ConcurrentDictionary Users { get; private set; } - public ConcurrentStack AllMessages { get; } + public ConcurrentStack AllMessages { get; private set; } public Message AddMessage(ReceivedMessage message) { if (!Users.TryGetValue(message.FromId, out var displayName)) { @@ -90,4 +95,43 @@ public class User { public string Id { get; set; } public string Name { get; set; } } + + public class RollingReplaySubject : ISubject { + private readonly ReplaySubject> _subjects; + private readonly IObservable _concatenatedSubjects; + private ISubject _currentSubject; + + public RollingReplaySubject() { + _subjects = new ReplaySubject>(1); + _concatenatedSubjects = _subjects.Concat(); + _currentSubject = new ReplaySubject(); + _subjects.OnNext(_currentSubject); + } + + public void Clear() { + _currentSubject.OnCompleted(); + _currentSubject = new ReplaySubject(); + _subjects.OnNext(_currentSubject); + } + + public void OnNext(T value) { + _currentSubject.OnNext(value); + } + + public void OnError(Exception error) { + _currentSubject.OnError(error); + } + + public void OnCompleted() { + _currentSubject.OnCompleted(); + _subjects.OnCompleted(); + // a quick way to make the current ReplaySubject unreachable + // except to in-flight observers, and not hold up collection + _currentSubject = new Subject(); + } + + public IDisposable Subscribe(IObserver observer) { + return _concatenatedSubjects.Subscribe(observer); + } + } } diff --git a/tests/GraphQL.Client.Tests.Common/Common.cs b/tests/GraphQL.Client.Tests.Common/Common.cs index e912d907..05673f23 100644 --- a/tests/GraphQL.Client.Tests.Common/Common.cs +++ b/tests/GraphQL.Client.Tests.Common/Common.cs @@ -5,9 +5,11 @@ namespace GraphQL.Client.Tests.Common { - public static class Common - { - public static StarWarsSchema GetStarWarsSchema() { + public static class Common { + public const string StarWarsEndpoint = "/graphql/starwars"; + public const string ChatEndpoint = "/graphql/chat"; + + public static StarWarsSchema GetStarWarsSchema() { var services = new ServiceCollection(); services.AddTransient(provider => new FuncDependencyResolver(provider.GetService)); services.AddStarWarsSchema(); @@ -33,7 +35,9 @@ public static void AddStarWarsSchema(this IServiceCollection services) { } public static void AddChatSchema(this IServiceCollection services) { - services.AddSingleton(); + var chat = new Chat.Schema.Chat(); + services.AddSingleton(chat); + services.AddSingleton(chat); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs new file mode 100644 index 00000000..3b20ccbe --- /dev/null +++ b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Http; +using GraphQL.Client.Serializer.Newtonsoft; +using GraphQL.Client.Serializer.SystemTextJson; +using GraphQL.Client.Tests.Common; +using GraphQL.Client.Tests.Common.Helpers; +using Microsoft.AspNetCore.Hosting; + +namespace GraphQL.Integration.Tests.Helpers { + public abstract class IntegrationServerTestFixture { + public int Port { get; private set; } + public IWebHost Server { get; private set; } + public abstract IGraphQLWebsocketJsonSerializer Serializer { get; } + + public IntegrationServerTestFixture() + { + Port = NetworkHelpers.GetFreeTcpPortNumber(); + CreateServer(); + } + + public void CreateServer() { + if(Server != null) + throw new InvalidOperationException("server is already created"); + Server = WebHostHelpers.CreateServer(Port); + } + + public async Task ShutdownServer() { + if (Server == null) + return; + + await Server.StopAsync(); + Server.Dispose(); + Server = null; + } + + public GraphQLHttpClient GetStarWarsClient(bool requestsViaWebsocket = false) + => GetGraphQLClient(Common.StarWarsEndpoint, requestsViaWebsocket); + + public GraphQLHttpClient GetChatClient(bool requestsViaWebsocket = false) + => GetGraphQLClient(Common.ChatEndpoint, requestsViaWebsocket); + + private GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false) { + if(Serializer == null) + throw new InvalidOperationException("JSON serializer not configured"); + return WebHostHelpers.GetGraphQLClient(Port, endpoint, requestsViaWebsocket, Serializer); + } + } + + public class NewtonsoftIntegrationServerTestFixture: IntegrationServerTestFixture { + public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new NewtonsoftJsonSerializer(); + } + + public class SystemTextJsonIntegrationServerTestFixture : IntegrationServerTestFixture { + public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer(); + } +} diff --git a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs index 0ee54a08..59084c6f 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs @@ -1,9 +1,12 @@ using System; +using System.Reactive.Disposables; using GraphQL.Client; using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; using GraphQL.Client.Serializer.Newtonsoft; +using GraphQL.Client.Tests.Common; using GraphQL.Client.Tests.Common.Helpers; +using IntegrationTestServer; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -12,7 +15,7 @@ namespace GraphQL.Integration.Tests.Helpers { public static class WebHostHelpers { - public static IWebHost CreateServer(int port) where TStartup : class + public static IWebHost CreateServer(int port) { var configBuilder = new ConfigurationBuilder(); configBuilder.AddInMemoryCollection(); @@ -25,41 +28,46 @@ public static IWebHost CreateServer(int port) where TStartup : class }) .UseConfiguration(config) .UseKestrel() - .UseStartup() + .UseStartup() .Build(); host.Start(); return host; } - - - public static GraphQLHttpClient GetGraphQLClient(int port, bool requestsViaWebsocket = false, IGraphQLWebsocketJsonSerializer serializer = null) + + public static GraphQLHttpClient GetGraphQLClient(int port, string endpoint, bool requestsViaWebsocket = false, IGraphQLWebsocketJsonSerializer serializer = null) => new GraphQLHttpClient(new GraphQLHttpClientOptions { - EndPoint = new Uri($"http://localhost:{port}/graphql"), + EndPoint = new Uri($"http://localhost:{port}{endpoint}"), UseWebSocketForQueriesAndMutations = requestsViaWebsocket, JsonSerializer = serializer ?? new NewtonsoftJsonSerializer() }); - - public static TestServerSetup SetupTest(bool requestsViaWebsocket = false, IGraphQLWebsocketJsonSerializer serializer = null) - where TStartup : class - { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - return new TestServerSetup { - Server = CreateServer(port), - Client = GetGraphQLClient(port, requestsViaWebsocket, serializer) - }; - } } public class TestServerSetup : IDisposable { + public TestServerSetup(IGraphQLWebsocketJsonSerializer serializer) { + this.serializer = serializer; + Port = NetworkHelpers.GetFreeTcpPortNumber(); + } + + public int Port { get; } public IWebHost Server { get; set; } - public GraphQLHttpClient Client { get; set; } + public IGraphQLWebsocketJsonSerializer serializer { get; set; } + + public GraphQLHttpClient GetStarWarsClient(bool requestsViaWebsocket = false) + => GetGraphQLClient(Common.StarWarsEndpoint, requestsViaWebsocket); + + public GraphQLHttpClient GetChatClient(bool requestsViaWebsocket = false) + => GetGraphQLClient(Common.ChatEndpoint, requestsViaWebsocket); + + private GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false) { + return WebHostHelpers.GetGraphQLClient(Port, endpoint, requestsViaWebsocket); + } + public void Dispose() { Server?.Dispose(); - Client?.Dispose(); } } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 304e2982..ea103901 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -1,41 +1,41 @@ -using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using FluentAssertions.Extensions; using GraphQL.Client.Abstractions; -using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; +using GraphQL.Client.Tests.Common.Chat.Schema; using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Client.Tests.Common.StarWars; using GraphQL.Integration.Tests.Helpers; -using IntegrationTestServer; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace GraphQL.Integration.Tests.QueryAndMutationTests { public abstract class Base { - protected IGraphQLWebsocketJsonSerializer serializer; + protected IntegrationServerTestFixture Fixture; + protected GraphQLHttpClient StarWarsClient; + protected GraphQLHttpClient ChatClient; - private TestServerSetup SetupTest(bool requestsViaWebsocket = false) => WebHostHelpers.SetupTest(requestsViaWebsocket, serializer); - - protected Base(IGraphQLWebsocketJsonSerializer serializer) { - this.serializer = serializer; + protected Base(IntegrationServerTestFixture fixture) { + Fixture = fixture; + StarWarsClient = Fixture.GetStarWarsClient(); + ChatClient = Fixture.GetChatClient(); } - + [Theory] [ClassData(typeof(StarWarsHumans))] public async void QueryTheory(int id, string name) { var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); - using (var setup = SetupTest()) { - var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty }}) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty }}) + .ConfigureAwait(false); - Assert.Null(response.Errors); - Assert.Equal(name, response.Data.Human.Name); - } + Assert.Null(response.Errors); + Assert.Equal(name, response.Data.Human.Name); } [Theory] @@ -43,13 +43,11 @@ public async void QueryTheory(int id, string name) { public async void QueryWithDynamicReturnTypeTheory(int id, string name) { var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); - using (var setup = SetupTest()) { - var response = await setup.Client.SendQueryAsync(graphQLRequest) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest) + .ConfigureAwait(false); - Assert.Null(response.Errors); - Assert.Equal(name, response.Data.human.name.ToString()); - } + Assert.Null(response.Errors); + Assert.Equal(name, response.Data.human.name.ToString()); } [Theory] @@ -63,13 +61,11 @@ query Human($id: String!){ }", new {id = id.ToString()}); - using (var setup = SetupTest()) { - var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) + .ConfigureAwait(false); - Assert.Null(response.Errors); - Assert.Equal(name, response.Data.Human.Name); - } + Assert.Null(response.Errors); + Assert.Equal(name, response.Data.Human.Name); } [Theory] @@ -90,13 +86,11 @@ query Droid($id: String!) { new { id = id.ToString() }, "Human"); - using (var setup = SetupTest()) { - var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) + .ConfigureAwait(false); - Assert.Null(response.Errors); - Assert.Equal(name, response.Data.Human.Name); - } + Assert.Null(response.Errors); + Assert.Equal(name, response.Data.Human.Name); } [Fact] @@ -118,27 +112,25 @@ query Human($id: String!){ } }"); - using (var setup = SetupTest()) { - var mutationResponse = await setup.Client.SendMutationAsync(mutationRequest, () => new { - createHuman = new { - Id = "", - Name = "", - HomePlanet = "" - } - }) - .ConfigureAwait(false); - - Assert.Null(mutationResponse.Errors); - Assert.Equal("Han Solo", mutationResponse.Data.createHuman.Name); - Assert.Equal("Corellia", mutationResponse.Data.createHuman.HomePlanet); - - queryRequest.Variables = new {id = mutationResponse.Data.createHuman.Id}; - var queryResponse = await setup.Client.SendQueryAsync(queryRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); - - Assert.Null(queryResponse.Errors); - Assert.Equal("Han Solo", queryResponse.Data.Human.Name); - } + var mutationResponse = await StarWarsClient.SendMutationAsync(mutationRequest, () => new { + createHuman = new { + Id = "", + Name = "", + HomePlanet = "" + } + }) + .ConfigureAwait(false); + + Assert.Null(mutationResponse.Errors); + Assert.Equal("Han Solo", mutationResponse.Data.createHuman.Name); + Assert.Equal("Corellia", mutationResponse.Data.createHuman.HomePlanet); + + queryRequest.Variables = new {id = mutationResponse.Data.createHuman.Id}; + var queryResponse = await StarWarsClient.SendQueryAsync(queryRequest, () => new { Human = new { Name = string.Empty } }) + .ConfigureAwait(false); + + Assert.Null(queryResponse.Errors); + Assert.Equal("Han Solo", queryResponse.Data.Human.Name); } [Fact] @@ -148,16 +140,14 @@ public async void PreprocessHttpRequestMessageIsCalled() { PreprocessHttpRequestMessage = callbackTester.Invoke }; - using (var setup = SetupTest()) { - var defaultHeaders = setup.Client.HttpClient.DefaultRequestHeaders; - var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); - callbackTester.CallbackShouldHaveBeenInvoked(message => { - Assert.Equal(defaultHeaders, message.Headers); - }); - Assert.Null(response.Errors); - Assert.Equal("Luke", response.Data.Human.Name); - } + var defaultHeaders = StarWarsClient.HttpClient.DefaultRequestHeaders; + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) + .ConfigureAwait(false); + callbackTester.CallbackShouldHaveBeenInvoked(message => { + Assert.Equal(defaultHeaders, message.Headers); + }); + Assert.Null(response.Errors); + Assert.Equal("Luke", response.Data.Human.Name); } [Fact] @@ -167,15 +157,37 @@ query Long { longRunning }"); - using (var setup = WebHostHelpers.SetupTest(false, serializer)) { - var cancellationTimeout = TimeSpan.FromSeconds(1); - var cts = new CancellationTokenSource(cancellationTimeout); - - Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new {longRunning = string.Empty}, cts.Token); - Action timeMeasurement = () => requestTask.Should().Throw(); - - timeMeasurement.ExecutionTime().Should().BeCloseTo(cancellationTimeout, TimeSpan.FromMilliseconds(50)); - } + var chatQuery = Fixture.Server.Services.GetService(); + var cts = new CancellationTokenSource(); + + var request = + ConcurrentTaskWrapper.New(() => ChatClient.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token)); + + // Test regular request + // start request + request.Start(); + // wait until the query has reached the server + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + // unblock the query + chatQuery.LongRunningQueryBlocker.Set(); + // check execution time + request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); + + // reset stuff + chatQuery.LongRunningQueryBlocker.Reset(); + request.Clear(); + + // cancellation test + request.Start(); + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + cts.Cancel(); + FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) + .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + + // let the server finish its query + chatQuery.LongRunningQueryBlocker.Set(); } + } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs index 1046ed6d..57f86f64 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs @@ -1,8 +1,10 @@ using GraphQL.Client.Serializer.Newtonsoft; +using GraphQL.Integration.Tests.Helpers; +using Xunit; namespace GraphQL.Integration.Tests.QueryAndMutationTests { - public class Newtonsoft: Base { - public Newtonsoft() : base(new NewtonsoftJsonSerializer()) + public class Newtonsoft: Base, IClassFixture { + public Newtonsoft(NewtonsoftIntegrationServerTestFixture fixture) : base(fixture) { } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs index dd725b4d..e74d5012 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs @@ -1,8 +1,10 @@ using GraphQL.Client.Serializer.SystemTextJson; +using GraphQL.Integration.Tests.Helpers; +using Xunit; namespace GraphQL.Integration.Tests.QueryAndMutationTests { - public class SystemTextJson: Base { - public SystemTextJson() : base(new SystemTextJsonSerializer()) + public class SystemTextJson: Base, IClassFixture { + public SystemTextJson(SystemTextJsonIntegrationServerTestFixture fixture) : base(fixture) { } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 0e1cfa6b..e4bd5010 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -2,59 +2,49 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net.WebSockets; -using System.Reactive.Concurrency; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Http; using GraphQL.Client.Tests.Common.Chat; using GraphQL.Client.Tests.Common.Chat.Schema; using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; -using IntegrationTestServer; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Reactive.Testing; using Xunit; using Xunit.Abstractions; namespace GraphQL.Integration.Tests.WebsocketTests { - public abstract class Base { + public abstract class Base: IAsyncLifetime { protected readonly ITestOutputHelper Output; - protected readonly IGraphQLWebsocketJsonSerializer Serializer; - protected IWebHost CreateServer(int port) => WebHostHelpers.CreateServer(port); - - protected Base(ITestOutputHelper output, IGraphQLWebsocketJsonSerializer serializer) { + protected readonly IntegrationServerTestFixture Fixture; + protected GraphQLHttpClient ChatClient; + + protected Base(ITestOutputHelper output, IntegrationServerTestFixture fixture) { this.Output = output; - this.Serializer = serializer; + this.Fixture = fixture; } - - [Fact] - public async void AssertTestingHarness() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - const string message = "some random testing message"; - var response = await client.AddMessageAsync(message).ConfigureAwait(false); + public async Task InitializeAsync() { + Fixture.Server.Services.GetService().Reset(); + ChatClient = Fixture.GetChatClient(true); + Output.WriteLine($"ChatClient: {ChatClient.GetHashCode()}"); + } - Assert.Equal(message, response.Data.AddMessage.Content); - } + public Task DisposeAsync() { + ChatClient?.Dispose(); + return Task.CompletedTask; } [Fact] public async void CanSendRequestViaWebsocket() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, true, Serializer); - const string message = "some random testing message"; - var response = await client.AddMessageAsync(message).ConfigureAwait(false); - - Assert.Equal(message, response.Data.AddMessage.Content); - } + await ChatClient.InitializeWebsocketConnection(); + const string message = "some random testing message"; + var response = await ChatClient.AddMessageAsync(message).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message); } [Fact] @@ -64,50 +54,44 @@ query Long { longRunning }"); - using (var setup = WebHostHelpers.SetupTest(true, Serializer)) { - await setup.Client.InitializeWebsocketConnection(); - var chatQuery = setup.Server.Services.GetService(); - var cts = new CancellationTokenSource(); - - var request = - ConcurrentTaskWrapper.New(() => setup.Client.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token)); - - // Test regular request - // start request - request.Start(); - // wait until the query has reached the server - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); - // unblock the query - chatQuery.LongRunningQueryBlocker.Set(); - // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); - request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); - - // reset stuff - chatQuery.LongRunningQueryBlocker.Reset(); - request.Clear(); - - // cancellation test - request.Start(); - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); - cts.Cancel(); - FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(100.Milliseconds()); - - // let the server finish its query - chatQuery.LongRunningQueryBlocker.Set(); - } + var chatQuery = Fixture.Server.Services.GetService(); + var cts = new CancellationTokenSource(); + + await ChatClient.InitializeWebsocketConnection(); + var request = + ConcurrentTaskWrapper.New(() => ChatClient.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token)); + + // Test regular request + // start request + request.Start(); + // wait until the query has reached the server + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + // unblock the query + chatQuery.LongRunningQueryBlocker.Set(); + // check execution time + request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); + + // reset stuff + chatQuery.LongRunningQueryBlocker.Reset(); + request.Clear(); + + // cancellation test + request.Start(); + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + cts.Cancel(); + FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) + .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + + // let the server finish its query + chatQuery.LongRunningQueryBlocker.Set(); } [Fact] public async void CanHandleRequestErrorViaWebsocket() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, true, Serializer); - var response = await client.SendQueryAsync("this query is formatted quite badly").ConfigureAwait(false); - - Assert.Single(response.Errors); - } + await ChatClient.InitializeWebsocketConnection(); + var response = await ChatClient.SendQueryAsync("this query is formatted quite badly").ConfigureAwait(false); + response.Errors.Should().ContainSingle("because the query is invalid"); } private const string SubscriptionQuery = @" @@ -122,36 +106,31 @@ public async void CanHandleRequestErrorViaWebsocket() { [Fact] public async void CanCreateObservableSubscription() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)){ - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - await client.InitializeWebsocketConnection(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - - Debug.WriteLine("creating subscription stream"); - IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest); - - Debug.WriteLine("subscribing..."); - using (var tester = observable.Monitor()) { - const string message1 = "Hello World"; - - var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) - .Which.Data.MessageAdded.Content.Should().Be(message1); - - const string message2 = "lorem ipsum dolor si amet"; - response = await client.AddMessageAsync(message2).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message2); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message2); - - // disposing the client should throw a TaskCanceledException on the subscription - client.Dispose(); - tester.Should().HaveCompleted(); - } - } + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + await ChatClient.InitializeWebsocketConnection(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + + Debug.WriteLine("creating subscription stream"); + var observable = ChatClient.CreateSubscriptionStream(SubscriptionRequest); + + Debug.WriteLine("subscribing..."); + using var tester = observable.Monitor(); + const string message1 = "Hello World"; + + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) + .Which.Data.MessageAdded.Content.Should().Be(message1); + + const string message2 = "lorem ipsum dolor si amet"; + response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message2); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message2); + + // disposing the client should throw a TaskCanceledException on the subscription + ChatClient.Dispose(); + tester.Should().HaveCompleted(); } public class MessageAddedSubscriptionResult { @@ -165,48 +144,45 @@ public class MessageAddedContent { [Fact] public async void CanReconnectWithSameObservable() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - - Debug.WriteLine("creating subscription stream"); - var observable = client.CreateSubscriptionStream(SubscriptionRequest); - - Debug.WriteLine("subscribing..."); - var tester = observable.Monitor(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - - const string message1 = "Hello World"; - var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message1); - - const string message2 = "How are you?"; - response = await client.AddMessageAsync(message2).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message2); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message2); - - Debug.WriteLine("disposing subscription..."); - tester.Dispose(); // does not close the websocket connection - - Debug.WriteLine("creating new subscription..."); - tester = observable.Monitor(); - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) - .Which.Data.MessageAdded.Content.Should().Be(message2); - - const string message3 = "lorem ipsum dolor si amet"; - response = await client.AddMessageAsync(message3).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message3); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message3); - - // disposing the client should complete the subscription - client.Dispose(); - tester.Should().HaveCompleted(); - } + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + + Debug.WriteLine("creating subscription stream"); + var observable = ChatClient.CreateSubscriptionStream(SubscriptionRequest); + + Debug.WriteLine("subscribing..."); + var tester = observable.Monitor(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + + const string message1 = "Hello World"; + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload(3.Seconds()) + .Which.Data.MessageAdded.Content.Should().Be(message1); + + const string message2 = "How are you?"; + response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message2); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message2); + + Debug.WriteLine("disposing subscription..."); + tester.Dispose(); // does not close the websocket connection + + Debug.WriteLine("creating new subscription..."); + var tester2 = observable.Monitor(); + tester2.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) + .Which.Data.MessageAdded.Content.Should().Be(message2); + + const string message3 = "lorem ipsum dolor si amet"; + response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message3); + tester2.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message3); + + // disposing the client should complete the subscription + ChatClient.Dispose(); + tester2.Should().HaveCompleted(); + tester2.Dispose(); } private const string SubscriptionQuery2 = @" @@ -235,75 +211,66 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { var port = NetworkHelpers.GetFreeTcpPortNumber(); var callbackTester = new CallbackMonitor(); var callbackTester2 = new CallbackMonitor(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - await client.InitializeWebsocketConnection(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - - Debug.WriteLine("creating subscription stream"); - IObservable> observable1 = - client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Invoke); - IObservable> observable2 = - client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Invoke); - - Debug.WriteLine("subscribing..."); - var tester = observable1.Monitor(); - var tester2 = observable2.Monitor(); - - const string message1 = "Hello World"; - var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message1); - - var joinResponse = await client.JoinDeveloperUser().ConfigureAwait(false); - joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); - - var payload = tester2.Should().HaveReceivedPayload().Subject; - payload.Data.UserJoined.Id.Should().Be("1", "because that's the id we sent with our mutation request"); - payload.Data.UserJoined.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); - Debug.WriteLine("disposing subscription..."); - tester2.Dispose(); - - const string message3 = "lorem ipsum dolor si amet"; - response = await client.AddMessageAsync(message3).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message3); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message3); - - // disposing the client should complete the subscription - client.Dispose(); - tester.Should().HaveCompleted(); - } + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + await ChatClient.InitializeWebsocketConnection(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + + Debug.WriteLine("creating subscription stream"); + var observable1 = ChatClient.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Invoke); + var observable2 = ChatClient.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Invoke); + + Debug.WriteLine("subscribing..."); + var tester = observable1.Monitor(); + var tester2 = observable2.Monitor(); + + const string message1 = "Hello World"; + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message1); + + var joinResponse = await ChatClient.JoinDeveloperUser().ConfigureAwait(false); + joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); + + var payload = tester2.Should().HaveReceivedPayload().Subject; + payload.Data.UserJoined.Id.Should().Be("1", "because that's the id we sent with our mutation request"); + payload.Data.UserJoined.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); + + Debug.WriteLine("disposing subscription..."); + tester2.Dispose(); + + const string message3 = "lorem ipsum dolor si amet"; + response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message3); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message3); + + // disposing the client should complete the subscription + ChatClient.Dispose(); + tester.Should().HaveCompleted(); } [Fact] public async void CanHandleConnectionTimeout() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - var server = CreateServer(port); var errorMonitor = new CallbackMonitor(); var reconnectBlocker = new ManualResetEventSlim(false); - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); // configure back-off strategy to allow it to be controlled from within the unit test - client.Options.BackOffStrategy = i => { + ChatClient.Options.BackOffStrategy = i => { reconnectBlocker.Wait(); return TimeSpan.Zero; }; var websocketStates = new ConcurrentQueue(); - using (client.WebsocketConnectionState.Subscribe(websocketStates.Enqueue)) { + using (ChatClient.WebsocketConnectionState.Subscribe(websocketStates.Enqueue)) { websocketStates.Should().ContainSingle(state => state == GraphQLWebsocketConnectionState.Disconnected); Debug.WriteLine("creating subscription stream"); - IObservable> observable = - client.CreateSubscriptionStream(SubscriptionRequest, - errorMonitor.Invoke); + var observable = ChatClient.CreateSubscriptionStream(SubscriptionRequest, errorMonitor.Invoke); Debug.WriteLine("subscribing..."); var tester = observable.Monitor(); @@ -317,21 +284,20 @@ public async void CanHandleConnectionTimeout() { websocketStates.Clear(); const string message1 = "Hello World"; - var response = await client.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); response.Data.AddMessage.Content.Should().Be(message1); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message1); Debug.WriteLine("stopping web host..."); - await server.StopAsync(CancellationToken.None).ConfigureAwait(false); - server.Dispose(); + await Fixture.ShutdownServer(); Debug.WriteLine("web host stopped..."); errorMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)) .Which.Should().BeOfType(); websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); - server = CreateServer(port); + Fixture.CreateServer(); reconnectBlocker.Set(); callbackMonitor.Should().HaveBeenInvokedWithPayload(); websocketStates.Should().ContainInOrder( @@ -340,71 +306,63 @@ public async void CanHandleConnectionTimeout() { GraphQLWebsocketConnectionState.Connected); // disposing the client should complete the subscription - client.Dispose(); + ChatClient.Dispose(); tester.Should().HaveCompleted(TimeSpan.FromSeconds(5)); - server.Dispose(); } } [Fact] public async void CanHandleSubscriptionError() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - await client.InitializeWebsocketConnection(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - Debug.WriteLine("creating subscription stream"); - IObservable> observable = client.CreateSubscriptionStream( - new GraphQLRequest(@" - subscription { - failImmediately { - content - } - }") - ); + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + await ChatClient.InitializeWebsocketConnection(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + Debug.WriteLine("creating subscription stream"); + IObservable> observable = ChatClient.CreateSubscriptionStream( + new GraphQLRequest(@" + subscription { + failImmediately { + content + } + }") + ); - Debug.WriteLine("subscribing..."); - using (var tester = observable.Monitor()) { - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) - .Which.Errors.Should().ContainSingle(); - tester.Should().HaveCompleted(); - client.Dispose(); - } + Debug.WriteLine("subscribing..."); + using (var tester = observable.Monitor()) { + tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) + .Which.Errors.Should().ContainSingle(); + tester.Should().HaveCompleted(); + ChatClient.Dispose(); } + } [Fact] public async void CanHandleQueryErrorInSubscription() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - - var test = new GraphQLRequest("tset", new { test = "blaa" }); - - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - await client.InitializeWebsocketConnection(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - Debug.WriteLine("creating subscription stream"); - IObservable> observable = client.CreateSubscriptionStream( - new GraphQLRequest(@" - subscription { - fieldDoesNotExist { - content - } - }") - ); - - Debug.WriteLine("subscribing..."); - using (var tester = observable.Monitor()) { - tester.Should().HaveReceivedPayload() - .Which.Errors.Should().ContainSingle(); - tester.Should().HaveCompleted(); - client.Dispose(); - } + var test = new GraphQLRequest("tset", new { test = "blaa" }); + + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + await ChatClient.InitializeWebsocketConnection(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + Debug.WriteLine("creating subscription stream"); + IObservable> observable = ChatClient.CreateSubscriptionStream( + new GraphQLRequest(@" + subscription { + fieldDoesNotExist { + content + } + }") + ); + + Debug.WriteLine("subscribing..."); + using (var tester = observable.Monitor()) { + tester.Should().HaveReceivedPayload() + .Which.Errors.Should().ContainSingle(); + tester.Should().HaveCompleted(); + ChatClient.Dispose(); } } + } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs index 02a0b030..76169663 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs @@ -1,10 +1,11 @@ using GraphQL.Client.Serializer.Newtonsoft; +using GraphQL.Integration.Tests.Helpers; +using Xunit; using Xunit.Abstractions; namespace GraphQL.Integration.Tests.WebsocketTests { - public class Newtonsoft: Base { - public Newtonsoft(ITestOutputHelper output) : base(output, new NewtonsoftJsonSerializer()) - { - } + public class Newtonsoft: Base, IClassFixture { + public Newtonsoft(ITestOutputHelper output, NewtonsoftIntegrationServerTestFixture fixture) : base(output, fixture) + { } } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs index 3a7882e4..fc7698ed 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs @@ -1,9 +1,10 @@ -using GraphQL.Client.Serializer.SystemTextJson; +using GraphQL.Integration.Tests.Helpers; +using Xunit; using Xunit.Abstractions; namespace GraphQL.Integration.Tests.WebsocketTests { - public class SystemTextJson: Base { - public SystemTextJson(ITestOutputHelper output) : base(output, new SystemTextJsonSerializer()) + public class SystemTextJson: Base, IClassFixture { + public SystemTextJson(ITestOutputHelper output, SystemTextJsonIntegrationServerTestFixture fixture) : base(output, fixture) { } } diff --git a/tests/IntegrationTestServer/Program.cs b/tests/IntegrationTestServer/Program.cs index 67cbd96b..ec3530e1 100644 --- a/tests/IntegrationTestServer/Program.cs +++ b/tests/IntegrationTestServer/Program.cs @@ -10,7 +10,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 0ae7280e..2f25f733 100644 --- a/tests/IntegrationTestServer/Startup.cs +++ b/tests/IntegrationTestServer/Startup.cs @@ -1,8 +1,11 @@ using GraphQL; +using GraphQL.Client.Tests.Common; +using GraphQL.Client.Tests.Common.Chat.Schema; using GraphQL.Server; using GraphQL.Server.Ui.GraphiQL; -using GraphQL.Server.Ui.Voyager; using GraphQL.Server.Ui.Playground; +using GraphQL.StarWars; +using GraphQL.Types; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -11,8 +14,8 @@ using Microsoft.Extensions.Hosting; namespace IntegrationTestServer { - public abstract class Startup { - protected Startup(IConfiguration configuration, IWebHostEnvironment environment) { + public class Startup { + public Startup(IConfiguration configuration, IWebHostEnvironment environment) { Configuration = configuration; Environment = environment; } @@ -29,9 +32,8 @@ public void ConfigureServices(IServiceCollection services) { }); services.AddTransient(provider => new FuncDependencyResolver(provider.GetService)); - - ConfigureGraphQLSchemaServices(services); - + services.AddChatSchema(); + services.AddStarWarsSchema(); services.AddGraphQL(options => { options.EnableMetrics = true; options.ExposeExceptions = Environment.IsDevelopment(); @@ -39,9 +41,6 @@ public void ConfigureServices(IServiceCollection services) { .AddWebSockets(); } - public abstract void ConfigureGraphQLSchemaServices(IServiceCollection services); - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -50,21 +49,23 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseWebSockets(); - ConfigureGraphQLSchema(app); + ConfigureGraphQLSchema(app, Common.ChatEndpoint); + ConfigureGraphQLSchema(app, Common.StarWarsEndpoint); app.UseGraphiQLServer(new GraphiQLOptions { GraphiQLPath = "/ui/graphiql", - GraphQLEndPoint = "/graphql" - }); - app.UseGraphQLVoyager(new GraphQLVoyagerOptions() { - GraphQLEndPoint = "/graphql", - Path = "/ui/voyager" + GraphQLEndPoint = Common.StarWarsEndpoint }); app.UseGraphQLPlayground(new GraphQLPlaygroundOptions { - Path = "/ui/playground" + Path = "/ui/playground", + GraphQLEndPoint = Common.ChatEndpoint }); } - public abstract void ConfigureGraphQLSchema(IApplicationBuilder app); + private void ConfigureGraphQLSchema(IApplicationBuilder app, string endpoint) where TSchema: Schema + { + app.UseGraphQLWebSockets(endpoint); + app.UseGraphQL(endpoint); + } } } diff --git a/tests/IntegrationTestServer/StartupChat.cs b/tests/IntegrationTestServer/StartupChat.cs deleted file mode 100644 index 2a782991..00000000 --- a/tests/IntegrationTestServer/StartupChat.cs +++ /dev/null @@ -1,22 +0,0 @@ -using GraphQL.Client.Tests.Common; -using GraphQL.Client.Tests.Common.Chat.Schema; -using GraphQL.Server; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace IntegrationTestServer { - public class StartupChat: Startup { - public StartupChat(IConfiguration configuration, IWebHostEnvironment environment): base(configuration, environment) { } - - public override void ConfigureGraphQLSchemaServices(IServiceCollection services) { - services.AddChatSchema(); - } - - public override void ConfigureGraphQLSchema(IApplicationBuilder app) { - app.UseGraphQLWebSockets("/graphql"); - app.UseGraphQL("/graphql"); - } - } -} diff --git a/tests/IntegrationTestServer/StartupStarWars.cs b/tests/IntegrationTestServer/StartupStarWars.cs deleted file mode 100644 index 15609515..00000000 --- a/tests/IntegrationTestServer/StartupStarWars.cs +++ /dev/null @@ -1,22 +0,0 @@ -using GraphQL.Client.Tests.Common; -using GraphQL.Server; -using GraphQL.StarWars; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace IntegrationTestServer { - public class StartupStarWars: Startup { - public StartupStarWars(IConfiguration configuration, IWebHostEnvironment environment): base(configuration, environment) { } - - public override void ConfigureGraphQLSchemaServices(IServiceCollection services) { - services.AddStarWarsSchema(); - } - - public override void ConfigureGraphQLSchema(IApplicationBuilder app) { - app.UseGraphQLWebSockets("/graphql"); - app.UseGraphQL("/graphql"); - } - } -} From da094ae55a0f0546282f3919336d32a83bf44814 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 26 Feb 2020 14:19:22 +0100 Subject: [PATCH 08/24] remove configure await --- .gitignore | 1 + .../Helpers/IntegrationServerTestFixture.cs | 8 ++-- .../Helpers/WebHostHelpers.cs | 12 ++++-- .../QueryAndMutationTests/Base.cs | 36 +++++++++-------- .../WebsocketTests/Base.cs | 39 +++++++++++-------- 5 files changed, 53 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index f173174b..be040ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea/ .vs/ +.vscode/ bin/ obj/ *.user diff --git a/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs index 3b20ccbe..e8344a13 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs @@ -17,13 +17,11 @@ public abstract class IntegrationServerTestFixture { public IntegrationServerTestFixture() { Port = NetworkHelpers.GetFreeTcpPortNumber(); - CreateServer(); } - public void CreateServer() { - if(Server != null) - throw new InvalidOperationException("server is already created"); - Server = WebHostHelpers.CreateServer(Port); + public async Task CreateServer() { + if (Server != null) return; + Server = await WebHostHelpers.CreateServer(Port); } public async Task ShutdownServer() { diff --git a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs index 59084c6f..923c11d1 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs @@ -1,6 +1,6 @@ using System; using System.Reactive.Disposables; -using GraphQL.Client; +using System.Threading.Tasks; using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; using GraphQL.Client.Serializer.Newtonsoft; @@ -9,13 +9,15 @@ using IntegrationTestServer; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace GraphQL.Integration.Tests.Helpers { public static class WebHostHelpers { - public static IWebHost CreateServer(int port) + public static async Task CreateServer(int port) { var configBuilder = new ConfigurationBuilder(); configBuilder.AddInMemoryCollection(); @@ -31,8 +33,10 @@ public static IWebHost CreateServer(int port) .UseStartup() .Build(); - host.Start(); - + var tcs = new TaskCompletionSource(); + host.Services.GetService().ApplicationStarted.Register(() => tcs.TrySetResult(true)); + await host.StartAsync(); + await tcs.Task; return host; } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index ea103901..92e61f8d 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -14,7 +14,7 @@ namespace GraphQL.Integration.Tests.QueryAndMutationTests { - public abstract class Base { + public abstract class Base: IAsyncLifetime { protected IntegrationServerTestFixture Fixture; protected GraphQLHttpClient StarWarsClient; @@ -22,17 +22,25 @@ public abstract class Base { protected Base(IntegrationServerTestFixture fixture) { Fixture = fixture; + } + + public async Task InitializeAsync() { + await Fixture.CreateServer(); StarWarsClient = Fixture.GetStarWarsClient(); ChatClient = Fixture.GetChatClient(); } - + + public Task DisposeAsync() { + ChatClient?.Dispose(); + StarWarsClient?.Dispose(); + return Task.CompletedTask; + } + [Theory] [ClassData(typeof(StarWarsHumans))] public async void QueryTheory(int id, string name) { var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); - - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty }}) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty }}); Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); @@ -43,8 +51,7 @@ public async void QueryTheory(int id, string name) { public async void QueryWithDynamicReturnTypeTheory(int id, string name) { var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); - var response = await StarWarsClient.SendQueryAsync(graphQLRequest) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest); Assert.Null(response.Errors); Assert.Equal(name, response.Data.human.name.ToString()); @@ -61,8 +68,7 @@ query Human($id: String!){ }", new {id = id.ToString()}); - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); @@ -86,8 +92,7 @@ query Droid($id: String!) { new { id = id.ToString() }, "Human"); - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); @@ -118,16 +123,14 @@ query Human($id: String!){ Name = "", HomePlanet = "" } - }) - .ConfigureAwait(false); + }); Assert.Null(mutationResponse.Errors); Assert.Equal("Han Solo", mutationResponse.Data.createHuman.Name); Assert.Equal("Corellia", mutationResponse.Data.createHuman.HomePlanet); queryRequest.Variables = new {id = mutationResponse.Data.createHuman.Id}; - var queryResponse = await StarWarsClient.SendQueryAsync(queryRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var queryResponse = await StarWarsClient.SendQueryAsync(queryRequest, () => new { Human = new { Name = string.Empty } }); Assert.Null(queryResponse.Errors); Assert.Equal("Han Solo", queryResponse.Data.Human.Name); @@ -141,8 +144,7 @@ public async void PreprocessHttpRequestMessageIsCalled() { }; var defaultHeaders = StarWarsClient.HttpClient.DefaultRequestHeaders; - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); callbackTester.CallbackShouldHaveBeenInvoked(message => { Assert.Equal(defaultHeaders, message.Headers); }); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index e4bd5010..d4c6ade2 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net.WebSockets; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; @@ -14,6 +15,7 @@ using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; using Xunit.Abstractions; @@ -29,9 +31,12 @@ protected Base(ITestOutputHelper output, IntegrationServerTestFixture fixture) { } public async Task InitializeAsync() { + await Fixture.CreateServer(); Fixture.Server.Services.GetService().Reset(); - ChatClient = Fixture.GetChatClient(true); - Output.WriteLine($"ChatClient: {ChatClient.GetHashCode()}"); + if (ChatClient == null) { + ChatClient = Fixture.GetChatClient(true); + Output.WriteLine($"ChatClient: {ChatClient.GetHashCode()}"); + } } public Task DisposeAsync() { @@ -43,7 +48,7 @@ public Task DisposeAsync() { public async void CanSendRequestViaWebsocket() { await ChatClient.InitializeWebsocketConnection(); const string message = "some random testing message"; - var response = await ChatClient.AddMessageAsync(message).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message); response.Data.AddMessage.Content.Should().Be(message); } @@ -90,7 +95,7 @@ query Long { [Fact] public async void CanHandleRequestErrorViaWebsocket() { await ChatClient.InitializeWebsocketConnection(); - var response = await ChatClient.SendQueryAsync("this query is formatted quite badly").ConfigureAwait(false); + var response = await ChatClient.SendQueryAsync("this query is formatted quite badly"); response.Errors.Should().ContainSingle("because the query is invalid"); } @@ -117,13 +122,13 @@ public async void CanCreateObservableSubscription() { using var tester = observable.Monitor(); const string message1 = "Hello World"; - var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) .Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "lorem ipsum dolor si amet"; - response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(false); + response = await ChatClient.AddMessageAsync(message2); response.Data.AddMessage.Content.Should().Be(message2); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message2); @@ -154,13 +159,13 @@ public async void CanReconnectWithSameObservable() { callbackMonitor.Should().HaveBeenInvokedWithPayload(); const string message1 = "Hello World"; - var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(3.Seconds()) + tester.Should().HaveReceivedPayload(10.Seconds()) .Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "How are you?"; - response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(false); + response = await ChatClient.AddMessageAsync(message2); response.Data.AddMessage.Content.Should().Be(message2); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message2); @@ -174,7 +179,7 @@ public async void CanReconnectWithSameObservable() { .Which.Data.MessageAdded.Content.Should().Be(message2); const string message3 = "lorem ipsum dolor si amet"; - response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(false); + response = await ChatClient.AddMessageAsync(message3); response.Data.AddMessage.Content.Should().Be(message3); tester2.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message3); @@ -225,12 +230,12 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { var tester2 = observable2.Monitor(); const string message1 = "Hello World"; - var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message1); - var joinResponse = await ChatClient.JoinDeveloperUser().ConfigureAwait(false); + var joinResponse = await ChatClient.JoinDeveloperUser(); joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); var payload = tester2.Should().HaveReceivedPayload().Subject; @@ -241,7 +246,7 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { tester2.Dispose(); const string message3 = "lorem ipsum dolor si amet"; - response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(false); + response = await ChatClient.AddMessageAsync(message3); response.Data.AddMessage.Content.Should().Be(message3); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message3); @@ -284,9 +289,9 @@ public async void CanHandleConnectionTimeout() { websocketStates.Clear(); const string message1 = "Hello World"; - var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload() + tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) .Which.Data.MessageAdded.Content.Should().Be(message1); Debug.WriteLine("stopping web host..."); @@ -297,9 +302,9 @@ public async void CanHandleConnectionTimeout() { .Which.Should().BeOfType(); websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); - Fixture.CreateServer(); + await InitializeAsync(); reconnectBlocker.Set(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)); websocketStates.Should().ContainInOrder( GraphQLWebsocketConnectionState.Disconnected, GraphQLWebsocketConnectionState.Connecting, From b218496d3854b47e588c0316cf1128dc3ec3be46 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 2 Mar 2020 15:51:32 +0100 Subject: [PATCH 09/24] fix reconnect test --- .../Chat/Schema/IChat.cs | 60 +++--------------- .../Helpers/ObservableTester.cs | 8 ++- .../WebsocketTests/Base.cs | 61 ++++++++++++------- 3 files changed, 53 insertions(+), 76 deletions(-) diff --git a/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs index 6d53e42b..9ebfbd1a 100644 --- a/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs @@ -16,22 +16,17 @@ public interface IChat { Message AddMessage(ReceivedMessage message); } - + public class Chat : IChat { - private readonly RollingReplaySubject _messageStream = new RollingReplaySubject(); - private readonly ISubject _userJoined = new Subject(); + private readonly ISubject messageStream = new ReplaySubject(1); + private readonly ISubject userJoined = new Subject(); public Chat() { - Reset(); - } - - public void Reset() { AllMessages = new ConcurrentStack(); Users = new ConcurrentDictionary { ["1"] = "developer", ["2"] = "tester" }; - _messageStream.Clear(); } public ConcurrentDictionary Users { get; private set; } @@ -55,7 +50,7 @@ public Message AddMessage(ReceivedMessage message) { public Message AddMessage(Message message) { AllMessages.Push(message); - _messageStream.OnNext(message); + messageStream.OnNext(message); return message; } @@ -69,12 +64,12 @@ public MessageFrom Join(string userId) { DisplayName = displayName }; - _userJoined.OnNext(joinedUser); + userJoined.OnNext(joinedUser); return joinedUser; } public IObservable Messages(string user) { - return _messageStream + return messageStream .Select(message => { message.Sub = user; return message; @@ -83,11 +78,11 @@ public IObservable Messages(string user) { } public void AddError(Exception exception) { - _messageStream.OnError(exception); + messageStream.OnError(exception); } public IObservable UserJoined() { - return _userJoined.AsObservable(); + return userJoined.AsObservable(); } } @@ -95,43 +90,4 @@ public class User { public string Id { get; set; } public string Name { get; set; } } - - public class RollingReplaySubject : ISubject { - private readonly ReplaySubject> _subjects; - private readonly IObservable _concatenatedSubjects; - private ISubject _currentSubject; - - public RollingReplaySubject() { - _subjects = new ReplaySubject>(1); - _concatenatedSubjects = _subjects.Concat(); - _currentSubject = new ReplaySubject(); - _subjects.OnNext(_currentSubject); - } - - public void Clear() { - _currentSubject.OnCompleted(); - _currentSubject = new ReplaySubject(); - _subjects.OnNext(_currentSubject); - } - - public void OnNext(T value) { - _currentSubject.OnNext(value); - } - - public void OnError(Exception error) { - _currentSubject.OnError(error); - } - - public void OnCompleted() { - _currentSubject.OnCompleted(); - _subjects.OnCompleted(); - // a quick way to make the current ReplaySubject unreachable - // except to in-flight observers, and not hold up collection - _currentSubject = new Subject(); - } - - public IDisposable Subscribe(IObserver observer) { - return _concatenatedSubjects.Subscribe(observer); - } - } } diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 43bcbe18..ec850d38 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; @@ -9,6 +10,7 @@ namespace GraphQL.Client.Tests.Common.Helpers { public class ObservableTester : IDisposable { private readonly IDisposable subscription; + private readonly EventLoopScheduler scheduler; private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); private readonly ManualResetEventSlim error = new ManualResetEventSlim(); @@ -34,12 +36,15 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - subscription = observable.ObserveOn(TaskPoolScheduler.Default).Subscribe( + scheduler = new EventLoopScheduler(); + subscription = observable.SubscribeOn(Scheduler.CurrentThread).ObserveOn(scheduler).Subscribe( obj => { + Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; updateReceived.Set(); }, ex => { + Debug.WriteLine($"observable tester {GetHashCode()} error received: {ex}"); Error = ex; error.Set(); }, @@ -57,6 +62,7 @@ private void Reset() { /// public void Dispose() { subscription?.Dispose(); + scheduler?.Dispose(); } public SubscriptionAssertions Should() { diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index d4c6ade2..bcfedb98 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -15,7 +15,6 @@ using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Xunit; using Xunit.Abstractions; @@ -30,12 +29,20 @@ protected Base(ITestOutputHelper output, IntegrationServerTestFixture fixture) { this.Fixture = fixture; } + protected static ReceivedMessage InitialMessage = new ReceivedMessage { + Content = "initial message", + SentAt = DateTime.Now, + FromId = "1" + }; + public async Task InitializeAsync() { await Fixture.CreateServer(); - Fixture.Server.Services.GetService().Reset(); + // make sure the buffer always contains the same message + Fixture.Server.Services.GetService().AddMessage(InitialMessage); + if (ChatClient == null) { + // then create the chat client ChatClient = Fixture.GetChatClient(true); - Output.WriteLine($"ChatClient: {ChatClient.GetHashCode()}"); } } @@ -120,18 +127,17 @@ public async void CanCreateObservableSubscription() { Debug.WriteLine("subscribing..."); using var tester = observable.Monitor(); - const string message1 = "Hello World"; + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + const string message1 = "Hello World"; var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) - .Which.Data.MessageAdded.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "lorem ipsum dolor si amet"; response = await ChatClient.AddMessageAsync(message2); response.Data.AddMessage.Content.Should().Be(message2); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message2); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); // disposing the client should throw a TaskCanceledException on the subscription ChatClient.Dispose(); @@ -157,32 +163,32 @@ public async void CanReconnectWithSameObservable() { Debug.WriteLine("subscribing..."); var tester = observable.Monitor(); callbackMonitor.Should().HaveBeenInvokedWithPayload(); + await ChatClient.InitializeWebsocketConnection(); + Debug.WriteLine("websocket connection initialized"); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); const string message1 = "Hello World"; + Debug.WriteLine($"adding message {message1}"); var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(10.Seconds()) - .Which.Data.MessageAdded.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "How are you?"; response = await ChatClient.AddMessageAsync(message2); response.Data.AddMessage.Content.Should().Be(message2); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message2); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); Debug.WriteLine("disposing subscription..."); tester.Dispose(); // does not close the websocket connection Debug.WriteLine("creating new subscription..."); var tester2 = observable.Monitor(); - tester2.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) - .Which.Data.MessageAdded.Content.Should().Be(message2); + tester2.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); const string message3 = "lorem ipsum dolor si amet"; response = await ChatClient.AddMessageAsync(message3); response.Data.AddMessage.Content.Should().Be(message3); - tester2.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message3); + tester2.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message3); // disposing the client should complete the subscription ChatClient.Dispose(); @@ -229,6 +235,8 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { var tester = observable1.Monitor(); var tester2 = observable2.Monitor(); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + const string message1 = "Hello World"; var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); @@ -265,7 +273,9 @@ public async void CanHandleConnectionTimeout() { var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); // configure back-off strategy to allow it to be controlled from within the unit test ChatClient.Options.BackOffStrategy = i => { + Debug.WriteLine("back-off strategy: waiting on reconnect blocker"); reconnectBlocker.Wait(); + Debug.WriteLine("back-off strategy: reconnecting..."); return TimeSpan.Zero; }; @@ -288,23 +298,28 @@ public async void CanHandleConnectionTimeout() { // clear the collection so the next tests on the collection work as expected websocketStates.Clear(); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + const string message1 = "Hello World"; var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) - .Which.Data.MessageAdded.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message1); Debug.WriteLine("stopping web host..."); await Fixture.ShutdownServer(); - Debug.WriteLine("web host stopped..."); + Debug.WriteLine("web host stopped"); - errorMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)) + errorMonitor.Should().HaveBeenInvokedWithPayload(10.Seconds()) .Which.Should().BeOfType(); websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); - + + Debug.WriteLine("restarting web host..."); await InitializeAsync(); + Debug.WriteLine("web host started"); reconnectBlocker.Set(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)); + callbackMonitor.Should().HaveBeenInvokedWithPayload(3.Seconds()); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + websocketStates.Should().ContainInOrder( GraphQLWebsocketConnectionState.Disconnected, GraphQLWebsocketConnectionState.Connecting, @@ -312,7 +327,7 @@ public async void CanHandleConnectionTimeout() { // disposing the client should complete the subscription ChatClient.Dispose(); - tester.Should().HaveCompleted(TimeSpan.FromSeconds(5)); + tester.Should().HaveCompleted(5.Seconds()); } } From 5e83a24551d5e231de8f5f78c8031964421125d3 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 3 Mar 2020 12:15:04 +0100 Subject: [PATCH 10/24] add debug output --- .../Websocket/GraphQLHttpWebSocket.cs | 1 + .../Websocket/GraphQLHttpWebsocketHelpers.cs | 7 ++--- .../Helpers/ObservableTester.cs | 26 +++++++++++++------ .../WebsocketTests/Base.cs | 8 +++--- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 1f824c8e..ce1d2ff5 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -179,6 +179,7 @@ private async Task _createResultStream(IObserver(); + Debug.WriteLine($"creating new response stream {responseSubject.GetHashCode()}"); // initialize and connect websocket await InitializeWebSocket().ConfigureAwait(false); diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index f4eeebd5..b74f373b 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -133,9 +133,10 @@ internal static IObservable> CreateSubscriptionStream } // throw exception on the observable to be caught by Retry() or complete sequence if cancellation was requested - return cancellationToken.IsCancellationRequested - ? Observable.Empty, Exception>>() - : Observable.Throw, Exception>>(e); + if (cancellationToken.IsCancellationRequested) + return Observable.Empty, Exception>>(); + else + return Observable.Throw, Exception>>(e); } catch (Exception exception) { // wrap all other exceptions to be propagated behind retry diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index ec850d38..00580f14 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -1,7 +1,5 @@ using System; using System.Diagnostics; -using System.Reactive.Concurrency; -using System.Reactive.Linq; using System.Threading; using FluentAssertions; using FluentAssertions.Execution; @@ -10,7 +8,6 @@ namespace GraphQL.Client.Tests.Common.Helpers { public class ObservableTester : IDisposable { private readonly IDisposable subscription; - private readonly EventLoopScheduler scheduler; private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); private readonly ManualResetEventSlim error = new ManualResetEventSlim(); @@ -36,8 +33,7 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - scheduler = new EventLoopScheduler(); - subscription = observable.SubscribeOn(Scheduler.CurrentThread).ObserveOn(scheduler).Subscribe( + subscription = observable.Subscribe( obj => { Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; @@ -48,8 +44,10 @@ public ObservableTester(IObservable observable) { Error = ex; error.Set(); }, - () => completed.Set() - ); + () => { + Debug.WriteLine($"observable tester {GetHashCode()}: completed"); + completed.Set(); + }); } /// @@ -62,7 +60,6 @@ private void Reset() { /// public void Dispose() { subscription?.Dispose(); - scheduler?.Dispose(); } public SubscriptionAssertions Should() { @@ -130,6 +127,19 @@ public AndConstraint> HaveCompleted(TimeSpan ti } public AndConstraint> HaveCompleted(string because = "", params object[] becauseArgs) => HaveCompleted(Subject.Timeout, because, becauseArgs); + + public AndConstraint> NotHaveCompleted(TimeSpan timeout, + string because = "", params object[] becauseArgs) { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject.completed.Wait(timeout)) + .ForCondition(isSet => !isSet) + .FailWith("Expected {context:Subscription} not to complete within {0}{reason}, but it did!", timeout); + + return new AndConstraint>(this); + } + public AndConstraint> NotHaveCompleted(string because = "", params object[] becauseArgs) + => NotHaveCompleted(Subject.Timeout, because, becauseArgs); } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index bcfedb98..062a3071 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -81,7 +81,7 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoking().ExecutionTime().Should().BeLessThan(500.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -93,7 +93,7 @@ query Long { chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + .ExecutionTime().Should().BeLessThan(500.Milliseconds()); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); @@ -312,12 +312,12 @@ public async void CanHandleConnectionTimeout() { errorMonitor.Should().HaveBeenInvokedWithPayload(10.Seconds()) .Which.Should().BeOfType(); websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); - + Debug.WriteLine("restarting web host..."); await InitializeAsync(); Debug.WriteLine("web host started"); reconnectBlocker.Set(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(3.Seconds()); + callbackMonitor.Should().HaveBeenInvokedWithPayload(10.Seconds()); tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); websocketStates.Should().ContainInOrder( From ce40801cb3c8a77651e09d3641093d8c637ff628 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 4 Mar 2020 14:52:26 +0100 Subject: [PATCH 11/24] consolidate naming, optimize threading in websocket --- .../IGraphQLWebsocketJsonSerializer.cs | 2 +- ...eWrapper.cs => WebsocketMessageWrapper.cs} | 2 +- .../NewtonsoftJsonSerializer.cs | 4 +- .../SystemTextJsonSerializer.cs | 4 +- .../Websocket/GraphQLHttpWebSocket.cs | 91 +++++++++++-------- .../Websocket/GraphQLHttpWebsocketHelpers.cs | 11 ++- .../Helpers/ObservableTester.cs | 4 +- .../WebsocketTests/Base.cs | 2 +- 8 files changed, 68 insertions(+), 52 deletions(-) rename src/GraphQL.Client.Abstractions.Websocket/{WebsocketResponseWrapper.cs => WebsocketMessageWrapper.cs} (69%) diff --git a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs index e2f445f5..376f45b6 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs @@ -10,7 +10,7 @@ namespace GraphQL.Client.Abstractions.Websocket public interface IGraphQLWebsocketJsonSerializer: IGraphQLJsonSerializer { byte[] SerializeToBytes(GraphQLWebSocketRequest request); - Task DeserializeToWebsocketResponseWrapperAsync(Stream stream); + Task DeserializeToWebsocketResponseWrapperAsync(Stream stream); GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes); } diff --git a/src/GraphQL.Client.Abstractions.Websocket/WebsocketResponseWrapper.cs b/src/GraphQL.Client.Abstractions.Websocket/WebsocketMessageWrapper.cs similarity index 69% rename from src/GraphQL.Client.Abstractions.Websocket/WebsocketResponseWrapper.cs rename to src/GraphQL.Client.Abstractions.Websocket/WebsocketMessageWrapper.cs index 5e90a38f..2a27677e 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/WebsocketResponseWrapper.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/WebsocketMessageWrapper.cs @@ -1,7 +1,7 @@ using System.Runtime.Serialization; namespace GraphQL.Client.Abstractions.Websocket { - public class WebsocketResponseWrapper : GraphQLWebSocketResponse { + public class WebsocketMessageWrapper : GraphQLWebSocketResponse { [IgnoreDataMember] public byte[] MessageBytes { get; set; } diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index a40ad456..7194a547 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -42,8 +42,8 @@ public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest re return Encoding.UTF8.GetBytes(json); } - public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { - return DeserializeFromUtf8Stream(stream); + public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { + return DeserializeFromUtf8Stream(stream); } public GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes) { diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index 420a8522..62be2e37 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -45,8 +45,8 @@ public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest re return JsonSerializer.SerializeToUtf8Bytes(new GraphQLWebSocketRequest(request), Options); } - public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { - return JsonSerializer.DeserializeAsync(stream, Options).AsTask(); + public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { + return JsonSerializer.DeserializeAsync(stream, Options).AsTask(); } public GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes) { diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index ce1d2ff5..fec8b75d 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -3,6 +3,7 @@ using System.IO; using System.Net.Http; using System.Net.WebSockets; +using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -23,9 +24,11 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly BehaviorSubject stateSubject = new BehaviorSubject(GraphQLWebsocketConnectionState.Disconnected); private readonly IDisposable requestSubscription; + private readonly EventLoopScheduler receiveLoopScheduler = new EventLoopScheduler(); + private readonly EventLoopScheduler sendLoopScheduler = new EventLoopScheduler(); private int connectionAttempt = 0; - private Subject responseSubject; + private Subject incomingMessagesSubject; private GraphQLHttpClientOptions Options => client.Options; #if NETFRAMEWORK @@ -39,27 +42,28 @@ internal class GraphQLHttpWebSocket : IDisposable { public IObservable ReceiveErrors => exceptionSubject.AsObservable(); public IObservable ConnectionState => stateSubject.DistinctUntilChanged(); - public IObservable ResponseStream { get; } + public IObservable ResponseStream { get; } public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { cancellationToken = cancellationTokenSource.Token; this.webSocketUri = webSocketUri; this.client = client; buffer = new ArraySegment(new byte[8192]); - ResponseStream = _createResponseStream(); + ResponseStream = GetMessageStream(); - requestSubscription = requestSubject.Select(request => Observable.FromAsync(() => _sendWebSocketRequest(request))).Concat().Subscribe(); + requestSubscription = requestSubject + .ObserveOn(sendLoopScheduler) + .Subscribe(async request => await SendWebSocketRequest(request)); } - - + #region Send requests - public Task SendWebSocketRequest(GraphQLWebSocketRequest request) { + public Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { requestSubject.OnNext(request); return request.SendTask(); } - private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request) { + private async Task SendWebSocketRequest(GraphQLWebSocketRequest request) { try { if (cancellationToken.IsCancellationRequested) { request.SendCanceled(); @@ -128,13 +132,13 @@ public Task InitializeWebSocket() { clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; #endif - return initializeWebSocketTask = _connectAsync(cancellationToken); + return initializeWebSocketTask = ConnectAsync(cancellationToken); } } - private async Task _connectAsync(CancellationToken token) { + private async Task ConnectAsync(CancellationToken token) { try { - await _backOff().ConfigureAwait(false); + await BackOff().ConfigureAwait(false); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting); Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); @@ -155,74 +159,79 @@ private async Task _connectAsync(CancellationToken token) { /// delay the next connection attempt using /// /// - private Task _backOff() { + private Task BackOff() { connectionAttempt++; if (connectionAttempt == 1) return Task.CompletedTask; var delay = Options.BackOffStrategy?.Invoke(connectionAttempt - 1) ?? TimeSpan.FromSeconds(5); Debug.WriteLine($"connection attempt #{connectionAttempt}, backing off for {delay.TotalSeconds} s"); - return Task.Delay(delay); + return Task.Delay(delay, cancellationToken); } - private IObservable _createResponseStream() { - return Observable.Create(_createResultStream) + private IObservable GetMessageStream() { + return Observable.Create(CreateMessageStream) // complete sequence on OperationCanceledException, this is triggered by the cancellation token on disposal - .Catch(exception => - Observable.Empty()); + .Catch(exception => + Observable.Empty()); } - private async Task _createResultStream(IObserver observer, CancellationToken token) { - cancellationToken.ThrowIfCancellationRequested(); + private async Task CreateMessageStream(IObserver observer, CancellationToken token) { + var cts = CancellationTokenSource.CreateLinkedTokenSource(token, cancellationToken); + cts.Token.ThrowIfCancellationRequested(); - if (responseSubject == null || responseSubject.IsDisposed) { + if (incomingMessagesSubject == null || incomingMessagesSubject.IsDisposed) { // create new response subject - responseSubject = new Subject(); - Debug.WriteLine($"creating new response stream {responseSubject.GetHashCode()}"); + incomingMessagesSubject = new Subject(); + Debug.WriteLine($"creating new incoming message stream {incomingMessagesSubject.GetHashCode()}"); // initialize and connect websocket await InitializeWebSocket().ConfigureAwait(false); // loop the receive task and subscribe the created subject to the results - Observable.Defer(() => _getReceiveTask().ToObservable()).Repeat().Subscribe(responseSubject); + Observable + .Defer(() => GetReceiveTask().ToObservable()) + .Repeat() + .SubscribeOn(receiveLoopScheduler) + .Subscribe(incomingMessagesSubject); // dispose the subject on any error or completion (will be recreated) - responseSubject.Subscribe(_ => { }, ex => { + incomingMessagesSubject.Subscribe(_ => { }, ex => { exceptionSubject.OnNext(ex); - responseSubject?.Dispose(); - responseSubject = null; + incomingMessagesSubject?.Dispose(); + incomingMessagesSubject = null; stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); }, () => { - responseSubject?.Dispose(); - responseSubject = null; + incomingMessagesSubject?.Dispose(); + incomingMessagesSubject = null; stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); }); } return new CompositeDisposable ( - responseSubject.Subscribe(observer), + incomingMessagesSubject.Subscribe(observer), Disposable.Create(() => { - Debug.WriteLine($"response stream {responseSubject.GetHashCode()} disposed"); + Debug.WriteLine($"incoming message stream {incomingMessagesSubject.GetHashCode()} disposed"); }) ); } - private Task receiveAsyncTask = null; + private Task receiveAsyncTask = null; private readonly object receiveTaskLocker = new object(); /// /// wrapper method to pick up the existing request task if already running /// /// - private Task _getReceiveTask() { + private Task GetReceiveTask() { lock (receiveTaskLocker) { cancellationToken.ThrowIfCancellationRequested(); if (receiveAsyncTask == null || receiveAsyncTask.IsFaulted || receiveAsyncTask.IsCompleted) - receiveAsyncTask = _receiveResultAsync(); + receiveAsyncTask = ReceiveWebsocketMessagesAsync(); } return receiveAsyncTask; @@ -232,9 +241,9 @@ private Task _getReceiveTask() { /// read a single message from the websocket /// /// - private async Task _receiveResultAsync() { + private async Task ReceiveWebsocketMessagesAsync() { try { - Debug.WriteLine($"receiving data on websocket {clientWebSocket.GetHashCode()} ..."); + Debug.WriteLine($"waiting for data on websocket {clientWebSocket.GetHashCode()} ..."); using (var ms = new MemoryStream()) { WebSocketReceiveResult webSocketReceiveResult = null; @@ -251,6 +260,7 @@ private async Task _receiveResultAsync() { if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); response.MessageBytes = ms.ToArray(); + Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} ..."); return response; } else { @@ -264,7 +274,7 @@ private async Task _receiveResultAsync() { } } - private async Task _closeAsync() { + private async Task CloseAsync() { if (clientWebSocket == null) return; @@ -304,12 +314,15 @@ private async Task CompleteAsync() { Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}..."); if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); - await _closeAsync().ConfigureAwait(false); + await CloseAsync().ConfigureAwait(false); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); - responseSubject?.OnCompleted(); - responseSubject?.Dispose(); + incomingMessagesSubject?.OnCompleted(); + incomingMessagesSubject?.Dispose(); + + sendLoopScheduler?.Dispose(); + receiveLoopScheduler?.Dispose(); stateSubject?.OnCompleted(); stateSubject?.Dispose(); diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index b74f373b..11f29bfc 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -4,6 +4,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; +using System.Text; using System.Threading; using System.Threading.Tasks; using GraphQL.Client.Abstractions.Websocket; @@ -85,7 +86,7 @@ internal static IObservable> CreateSubscriptionStream try { Debug.WriteLine($"sending close message on subscription {startRequest.Id}"); - await graphQlHttpWebSocket.SendWebSocketRequest(closeRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(closeRequest).ConfigureAwait(false); } // do not break on disposing catch (OperationCanceledException) { } @@ -95,7 +96,7 @@ internal static IObservable> CreateSubscriptionStream // send connection init Debug.WriteLine($"sending connection init on subscription {startRequest.Id}"); try { - await graphQlHttpWebSocket.SendWebSocketRequest(initRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(initRequest).ConfigureAwait(false); } catch (Exception e) { Console.WriteLine(e); @@ -105,7 +106,7 @@ internal static IObservable> CreateSubscriptionStream Debug.WriteLine($"sending initial message on subscription {startRequest.Id}"); // send subscription request try { - await graphQlHttpWebSocket.SendWebSocketRequest(startRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(startRequest).ConfigureAwait(false); } catch (Exception e) { Console.WriteLine(e); @@ -198,7 +199,7 @@ internal static Task> SendRequest( Debug.WriteLine($"submitting request {websocketRequest.Id}"); // send request try { - await graphQlHttpWebSocket.SendWebSocketRequest(websocketRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(websocketRequest).ConfigureAwait(false); } catch (Exception e) { Console.WriteLine(e); @@ -210,7 +211,7 @@ internal static Task> SendRequest( // complete sequence on OperationCanceledException, this is triggered by the cancellation token .Catch, OperationCanceledException>(exception => Observable.Empty>()) - .FirstOrDefaultAsync() + .FirstAsync() .ToTask(cancellationToken); } } diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 00580f14..4d070e71 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.Reactive.Concurrency; +using System.Reactive.Linq; using System.Threading; using FluentAssertions; using FluentAssertions.Execution; @@ -33,7 +35,7 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - subscription = observable.Subscribe( + subscription = observable.SubscribeOn(Scheduler.Default).Subscribe( obj => { Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 062a3071..662ca85b 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -317,7 +317,7 @@ public async void CanHandleConnectionTimeout() { await InitializeAsync(); Debug.WriteLine("web host started"); reconnectBlocker.Set(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(10.Seconds()); + callbackMonitor.Should().HaveBeenInvokedWithPayload(3.Seconds()); tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); websocketStates.Should().ContainInOrder( From a6dbfb9732515653bd3d65e3ed5522f555ff0108 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 4 Mar 2020 15:02:47 +0100 Subject: [PATCH 12/24] allow 500ms for cancellation --- tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 92e61f8d..773e4feb 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -173,7 +173,7 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoking().ExecutionTime().Should().BeLessThan(500.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -185,7 +185,7 @@ query Long { chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + .ExecutionTime().Should().BeLessThan(500.Milliseconds()); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); From b501c04e51471cbfd339464a284bf72e38ca107a Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 4 Mar 2020 15:09:01 +0100 Subject: [PATCH 13/24] extend default timeout --- tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs | 2 +- tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 4d070e71..1d468deb 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -17,7 +17,7 @@ public class ObservableTester : IDisposable { /// /// The timeout for . Defaults to 1 s /// - public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1); + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(3); /// /// Indicates that an update has been received since the last diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 773e4feb..415c7f32 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -169,7 +169,7 @@ query Long { // start request request.Start(); // wait until the query has reached the server - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time @@ -182,7 +182,7 @@ query Long { // cancellation test request.Start(); - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) .ExecutionTime().Should().BeLessThan(500.Milliseconds()); From 7837eddd97a8a11fcc14573c6aeb5efb83cf6cb8 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 09:28:13 +0100 Subject: [PATCH 14/24] remove ConfigureAwait --- .../GraphQLLocalExecutionClient.cs | 8 +-- .../Websocket/GraphQLHttpWebSocket.cs | 66 +++++++++++-------- .../Websocket/GraphQLHttpWebsocketHelpers.cs | 21 +++--- .../BaseSerializerTest.cs | 6 +- .../Helpers/CallbackMonitor.cs | 43 +++--------- .../Helpers/ObservableTester.cs | 12 +++- .../QueryAndMutationTests/Base.cs | 4 +- .../WebsocketTests/Base.cs | 1 + 8 files changed, 81 insertions(+), 80 deletions(-) diff --git a/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs index d73e2cc6..6624020c 100644 --- a/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs +++ b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs @@ -76,11 +76,11 @@ public IObservable> CreateSubscriptionStream> ExecuteQueryAsync(GraphQLRequest request, CancellationToken cancellationToken) { - var executionResult = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false); - return await ExecutionResultToGraphQLResponse(executionResult, cancellationToken).ConfigureAwait(false); + var executionResult = await ExecuteAsync(request, cancellationToken); + return await ExecutionResultToGraphQLResponse(executionResult, cancellationToken); } private async Task>> ExecuteSubscriptionAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { - var result = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false); + var result = await ExecuteAsync(request, cancellationToken); return ((SubscriptionExecutionResult)result).Streams?.Values.SingleOrDefault()? .SelectMany(executionResult => Observable.FromAsync(token => ExecutionResultToGraphQLResponse(executionResult, token))); } @@ -100,7 +100,7 @@ private async Task ExecuteAsync(GraphQLRequest request, Cancell options.Query = request.Query; options.Inputs = inputs; options.CancellationToken = cancellationToken; - }).ConfigureAwait(false); + }); return result; } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index fec8b75d..fccf7894 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -14,6 +14,9 @@ namespace GraphQL.Client.Http.Websocket { internal class GraphQLHttpWebSocket : IDisposable { + + #region Private fields + private readonly Uri webSocketUri; private readonly GraphQLHttpClient client; private readonly ArraySegment buffer; @@ -29,6 +32,7 @@ internal class GraphQLHttpWebSocket : IDisposable { private int connectionAttempt = 0; private Subject incomingMessagesSubject; + private IDisposable incomingMessagesDisposable; private GraphQLHttpClientOptions Options => client.Options; #if NETFRAMEWORK @@ -37,11 +41,11 @@ internal class GraphQLHttpWebSocket : IDisposable { private ClientWebSocket clientWebSocket = null; #endif - + #endregion + public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None; public IObservable ReceiveErrors => exceptionSubject.AsObservable(); public IObservable ConnectionState => stateSubject.DistinctUntilChanged(); - public IObservable ResponseStream { get; } public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { @@ -50,6 +54,8 @@ public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { this.client = client; buffer = new ArraySegment(new byte[8192]); ResponseStream = GetMessageStream(); + receiveLoopScheduler.Schedule(() => + Debug.WriteLine($"receive loop scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); requestSubscription = requestSubject .ObserveOn(sendLoopScheduler) @@ -70,13 +76,13 @@ private async Task SendWebSocketRequest(GraphQLWebSocketRequest request) { return; } - await InitializeWebSocket().ConfigureAwait(false); + await InitializeWebSocket(); var requestBytes = Options.JsonSerializer.SerializeToBytes(request); await this.clientWebSocket.SendAsync( new ArraySegment(requestBytes), WebSocketMessageType.Text, true, - cancellationToken).ConfigureAwait(false); + cancellationToken); request.SendCompleted(); } catch (Exception e) { @@ -138,10 +144,10 @@ public Task InitializeWebSocket() { private async Task ConnectAsync(CancellationToken token) { try { - await BackOff().ConfigureAwait(false); + await BackOff(); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting); Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); - await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); + await clientWebSocket.ConnectAsync(webSocketUri, token); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected); Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}, invoking Options.OnWebsocketConnected()"); await (Options.OnWebsocketConnected?.Invoke(client) ?? Task.CompletedTask); @@ -181,42 +187,49 @@ private async Task CreateMessageStream(IObserver(); Debug.WriteLine($"creating new incoming message stream {incomingMessagesSubject.GetHashCode()}"); // initialize and connect websocket - await InitializeWebSocket().ConfigureAwait(false); + await InitializeWebSocket(); // loop the receive task and subscribe the created subject to the results - Observable + var receiveLoopSubscription = Observable .Defer(() => GetReceiveTask().ToObservable()) .Repeat() - .SubscribeOn(receiveLoopScheduler) .Subscribe(incomingMessagesSubject); + + incomingMessagesDisposable = new CompositeDisposable( + incomingMessagesSubject, + receiveLoopSubscription, + Disposable.Create(() => { + Debug.WriteLine($"incoming message stream {incomingMessagesSubject.GetHashCode()} disposed"); + })); // dispose the subject on any error or completion (will be recreated) incomingMessagesSubject.Subscribe(_ => { }, ex => { exceptionSubject.OnNext(ex); - incomingMessagesSubject?.Dispose(); + incomingMessagesDisposable?.Dispose(); incomingMessagesSubject = null; stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); }, () => { - incomingMessagesSubject?.Dispose(); + incomingMessagesDisposable?.Dispose(); incomingMessagesSubject = null; stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); }); } - return new CompositeDisposable - ( - incomingMessagesSubject.Subscribe(observer), - Disposable.Create(() => { - Debug.WriteLine($"incoming message stream {incomingMessagesSubject.GetHashCode()} disposed"); - }) - ); + var subscription = new CompositeDisposable(incomingMessagesSubject.Subscribe(observer)); + var hashCode = subscription.GetHashCode(); + subscription.Add(Disposable.Create(() => { + Debug.WriteLine($"incoming message subscription {hashCode} disposed"); + })); + Debug.WriteLine($"new incoming message subscription {hashCode} created"); + return subscription; } private Task receiveAsyncTask = null; @@ -243,7 +256,7 @@ private Task GetReceiveTask() { /// private async Task ReceiveWebsocketMessagesAsync() { try { - Debug.WriteLine($"waiting for data on websocket {clientWebSocket.GetHashCode()} ..."); + Debug.WriteLine($"waiting for data on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); using (var ms = new MemoryStream()) { WebSocketReceiveResult webSocketReceiveResult = null; @@ -260,7 +273,7 @@ private async Task ReceiveWebsocketMessagesAsync() { if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); response.MessageBytes = ms.ToArray(); - Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} ..."); + Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); return response; } else { @@ -287,7 +300,7 @@ private async Task CloseAsync() { } Debug.WriteLine($"closing websocket {clientWebSocket.GetHashCode()}"); - await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); + await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); } @@ -314,15 +327,12 @@ private async Task CompleteAsync() { Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}..."); if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); - await CloseAsync().ConfigureAwait(false); + await CloseAsync(); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); incomingMessagesSubject?.OnCompleted(); - incomingMessagesSubject?.Dispose(); - - sendLoopScheduler?.Dispose(); - receiveLoopScheduler?.Dispose(); + incomingMessagesDisposable?.Dispose(); stateSubject?.OnCompleted(); stateSubject?.Dispose(); @@ -330,6 +340,10 @@ private async Task CompleteAsync() { exceptionSubject?.OnCompleted(); exceptionSubject?.Dispose(); cancellationTokenSource.Dispose(); + + sendLoopScheduler?.Dispose(); + receiveLoopScheduler?.Dispose(); + Debug.WriteLine($"websocket {clientWebSocket.GetHashCode()} disposed"); } #endregion diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index 11f29bfc..82fc6f2d 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Net.WebSockets; +using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; @@ -19,6 +20,7 @@ internal static IObservable> CreateSubscriptionStream CancellationToken cancellationToken = default) { return Observable.Defer(() => Observable.Create>(async observer => { + Debug.WriteLine($"Create observable thread id: {Thread.CurrentThread.ManagedThreadId}"); await client.Options.PreprocessRequest(request, client); var startRequest = new GraphQLWebSocketRequest { Id = Guid.NewGuid().ToString("N"), @@ -47,7 +49,7 @@ internal static IObservable> CreateSubscriptionStream } // post the GraphQLResponse to the stream (even if a GraphQL error occurred) - Debug.WriteLine($"received payload on subscription {startRequest.Id}"); + Debug.WriteLine($"received payload on subscription {startRequest.Id} (thread {Thread.CurrentThread.ManagedThreadId})"); var typedResponse = client.Options.JsonSerializer.DeserializeToWebsocketResponse( response.MessageBytes); @@ -71,7 +73,7 @@ internal static IObservable> CreateSubscriptionStream try { // initialize websocket (completes immediately if socket is already open) - await graphQlHttpWebSocket.InitializeWebSocket().ConfigureAwait(false); + await graphQlHttpWebSocket.InitializeWebSocket(); } catch (Exception e) { // subscribe observer to failed observable @@ -86,7 +88,7 @@ internal static IObservable> CreateSubscriptionStream try { Debug.WriteLine($"sending close message on subscription {startRequest.Id}"); - await graphQlHttpWebSocket.QueueWebSocketRequest(closeRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(closeRequest); } // do not break on disposing catch (OperationCanceledException) { } @@ -96,7 +98,7 @@ internal static IObservable> CreateSubscriptionStream // send connection init Debug.WriteLine($"sending connection init on subscription {startRequest.Id}"); try { - await graphQlHttpWebSocket.QueueWebSocketRequest(initRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(initRequest); } catch (Exception e) { Console.WriteLine(e); @@ -106,7 +108,7 @@ internal static IObservable> CreateSubscriptionStream Debug.WriteLine($"sending initial message on subscription {startRequest.Id}"); // send subscription request try { - await graphQlHttpWebSocket.QueueWebSocketRequest(startRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(startRequest); } catch (Exception e) { Console.WriteLine(e); @@ -136,8 +138,10 @@ internal static IObservable> CreateSubscriptionStream // throw exception on the observable to be caught by Retry() or complete sequence if cancellation was requested if (cancellationToken.IsCancellationRequested) return Observable.Empty, Exception>>(); - else + else { + Debug.WriteLine($"Catch handler thread id: {Thread.CurrentThread.ManagedThreadId}"); return Observable.Throw, Exception>>(e); + } } catch (Exception exception) { // wrap all other exceptions to be propagated behind retry @@ -148,6 +152,7 @@ internal static IObservable> CreateSubscriptionStream .Retry() // unwrap and push results or throw wrapped exceptions .SelectMany(t => { + Debug.WriteLine($"unwrap exception thread id: {Thread.CurrentThread.ManagedThreadId}"); // if the result contains an exception, throw it on the observable if (t.Item2 != null) return Observable.Throw>(t.Item2); @@ -185,7 +190,7 @@ internal static Task> SendRequest( try { // intialize websocket (completes immediately if socket is already open) - await graphQlHttpWebSocket.InitializeWebSocket().ConfigureAwait(false); + await graphQlHttpWebSocket.InitializeWebSocket(); } catch (Exception e) { // subscribe observer to failed observable @@ -199,7 +204,7 @@ internal static Task> SendRequest( Debug.WriteLine($"submitting request {websocketRequest.Id}"); // send request try { - await graphQlHttpWebSocket.QueueWebSocketRequest(websocketRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(websocketRequest); } catch (Exception e) { Console.WriteLine(e); diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs index a07f38f1..579e756e 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs @@ -46,7 +46,7 @@ public async void CanDeserializeExtensions() { var response = await ChatClient.SendQueryAsync(new GraphQLRequest("query { extensionsTest }"), () => new { extensionsTest = "" }) - .ConfigureAwait(false); + ; response.Errors.Should().NotBeNull(); response.Errors.Should().ContainSingle(); @@ -76,7 +76,7 @@ query Droid($id: String!) { "Human"); var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + ; Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); @@ -85,7 +85,7 @@ query Droid($id: String!) { [Fact] public async void CanDoSerializationWithPredefinedTypes() { const string message = "some random testing message"; - var response = await ChatClient.AddMessageAsync(message).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message); Assert.Equal(message, response.Data.AddMessage.Content); } diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs index 4b623ac6..2a27a259 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading; using FluentAssertions; using FluentAssertions.Execution; @@ -24,40 +25,10 @@ public class CallbackMonitor { public void Invoke(T param) { LastPayload = param; + Debug.WriteLine($"CallbackMonitor invoke handler thread id: {Thread.CurrentThread.ManagedThreadId}"); callbackInvoked.Set(); } - - /// - /// Asserts that a new update has been pushed to the within the configured since the last . - /// If supplied, the action is executed on the submitted payload. - /// - /// action to assert the contents of the payload - public void CallbackShouldHaveBeenInvoked(Action assertPayload = null, TimeSpan? timeout = null) { - try { - callbackInvoked.Wait(timeout ?? Timeout).Should().BeTrue("because the callback method should have been invoked (timeout: {0} s)", - (timeout ?? Timeout).TotalSeconds); - - assertPayload?.Invoke(LastPayload); - } - finally { - Reset(); - } - } - - /// - /// Asserts that no new update has been pushed within the given since the last - /// - /// the time in ms in which no new update must be pushed to the . defaults to 100 - public void CallbackShouldNotHaveBeenInvoked(TimeSpan? timeout = null) { - if (!timeout.HasValue) timeout = TimeSpan.FromMilliseconds(100); - try { - callbackInvoked.Wait(timeout.Value).Should().BeFalse("because the callback method should not have been invoked"); - } - finally { - Reset(); - } - } - + /// /// Resets the tester class. Should be called before triggering the potential update /// @@ -65,8 +36,7 @@ public void Reset() { LastPayload = default(T); callbackInvoked.Reset(); } - - + public CallbackAssertions Should() { return new CallbackAssertions(this); } @@ -82,7 +52,10 @@ public AndWhichConstraint, TPayload> HaveBeenInvoke string because = "", params object[] becauseArgs) { Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => Subject.callbackInvoked.Wait(timeout)) + .Given(() => { + Debug.WriteLine($"HaveBeenInvokedWithPayload thread id: {Thread.CurrentThread.ManagedThreadId}"); + return Subject.callbackInvoked.Wait(timeout); + }) .ForCondition(isSet => isSet) .FailWith("Expected {context:callback} to be invoked{reason}, but did not receive a call within {0}", timeout); diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 1d468deb..86ec3a1f 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -13,6 +13,8 @@ public class ObservableTester : IDisposable { private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); private readonly ManualResetEventSlim error = new ManualResetEventSlim(); + private readonly EventLoopScheduler subscriptionScheduler = new EventLoopScheduler(); + private readonly EventLoopScheduler observeScheduler = new EventLoopScheduler(); /// /// The timeout for . Defaults to 1 s @@ -35,7 +37,13 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - subscription = observable.SubscribeOn(Scheduler.Default).Subscribe( + subscriptionScheduler.Schedule(() => + Debug.WriteLine($"Subscription scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); + + observeScheduler.Schedule(() => + Debug.WriteLine($"Observe scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); + + subscription = observable.SubscribeOn(subscriptionScheduler).ObserveOn(observeScheduler).Subscribe( obj => { Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; @@ -62,6 +70,8 @@ private void Reset() { /// public void Dispose() { subscription?.Dispose(); + subscriptionScheduler.Dispose(); + observeScheduler.Dispose(); } public SubscriptionAssertions Should() { diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 415c7f32..c25117ca 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -145,9 +145,7 @@ public async void PreprocessHttpRequestMessageIsCalled() { var defaultHeaders = StarWarsClient.HttpClient.DefaultRequestHeaders; var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); - callbackTester.CallbackShouldHaveBeenInvoked(message => { - Assert.Equal(defaultHeaders, message.Headers); - }); + callbackTester.Should().HaveBeenInvokedWithPayload().Which.Headers.Should().BeEquivalentTo(defaultHeaders); Assert.Null(response.Errors); Assert.Equal("Luke", response.Data.Human.Name); } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 662ca85b..47d2e5e8 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -284,6 +284,7 @@ public async void CanHandleConnectionTimeout() { using (ChatClient.WebsocketConnectionState.Subscribe(websocketStates.Enqueue)) { websocketStates.Should().ContainSingle(state => state == GraphQLWebsocketConnectionState.Disconnected); + Debug.WriteLine($"Test method thread id: {Thread.CurrentThread.ManagedThreadId}"); Debug.WriteLine("creating subscription stream"); var observable = ChatClient.CreateSubscriptionStream(SubscriptionRequest, errorMonitor.Invoke); From 2c4aec72b13d29ada7c6e13a2abb0aa9f3422cc0 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 10:06:56 +0100 Subject: [PATCH 15/24] eliminate helpers class --- src/GraphQL.Client/GraphQLHttpClient.cs | 6 +- .../Websocket/GraphQLHttpWebSocket.cs | 275 ++++++++++++++++-- .../Websocket/GraphQLHttpWebsocketHelpers.cs | 223 -------------- 3 files changed, 258 insertions(+), 246 deletions(-) delete mode 100644 src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index a4da13c6..30e85fd0 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -66,7 +66,7 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient /// public Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { return Options.UseWebSocketForQueriesAndMutations - ? this.graphQlHttpWebSocket.SendRequest(request, this, cancellationToken) + ? this.graphQlHttpWebSocket.SendRequest(request, cancellationToken) : this.SendHttpPostRequestAsync(request, cancellationToken); } @@ -85,7 +85,7 @@ public IObservable> CreateSubscriptionStream>)subscriptionStreams[key]; - var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, this, cancellationToken: cancellationTokenSource.Token); + var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request); subscriptionStreams.TryAdd(key, observable); return observable; @@ -101,7 +101,7 @@ public IObservable> CreateSubscriptionStream>)subscriptionStreams[key]; - var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, this, exceptionHandler, cancellationTokenSource.Token); + var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, exceptionHandler); subscriptionStreams.TryAdd(key, observable); return observable; } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index fccf7894..61462a10 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -20,8 +20,8 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly Uri webSocketUri; private readonly GraphQLHttpClient client; private readonly ArraySegment buffer; - private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - private readonly CancellationToken cancellationToken; + private readonly CancellationTokenSource internalCancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationToken internalCancellationToken; private readonly Subject requestSubject = new Subject(); private readonly Subject exceptionSubject = new Subject(); private readonly BehaviorSubject stateSubject = @@ -34,6 +34,9 @@ internal class GraphQLHttpWebSocket : IDisposable { private Subject incomingMessagesSubject; private IDisposable incomingMessagesDisposable; private GraphQLHttpClientOptions Options => client.Options; + + private Task initializeWebSocketTask = Task.CompletedTask; + private readonly object initializeLock = new object(); #if NETFRAMEWORK private WebSocket clientWebSocket = null; @@ -42,18 +45,39 @@ internal class GraphQLHttpWebSocket : IDisposable { #endif #endregion + + #region Public properties + + /// + /// The current websocket state + /// public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None; + + /// + /// Publishes all errors which occur within the receive pipeline + /// public IObservable ReceiveErrors => exceptionSubject.AsObservable(); + + /// + /// Publishes the connection state of the + /// public IObservable ConnectionState => stateSubject.DistinctUntilChanged(); - public IObservable ResponseStream { get; } + /// + /// Publishes all messages which are received on the websocket + /// + public IObservable IncomingMessageStream { get; } + + #endregion + + public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { - cancellationToken = cancellationTokenSource.Token; + internalCancellationToken = internalCancellationTokenSource.Token; this.webSocketUri = webSocketUri; this.client = client; buffer = new ArraySegment(new byte[8192]); - ResponseStream = GetMessageStream(); + IncomingMessageStream = GetMessageStream(); receiveLoopScheduler.Schedule(() => Debug.WriteLine($"receive loop scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); @@ -61,9 +85,223 @@ public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { .ObserveOn(sendLoopScheduler) .Subscribe(async request => await SendWebSocketRequest(request)); } - + + #region Send requests + /// + /// Create a new subscription stream + /// + /// the response type + /// the to start the subscription + /// Optional: exception handler for handling exceptions within the receive pipeline + /// a which represents the subscription + public IObservable> CreateSubscriptionStream(GraphQLRequest request, Action exceptionHandler = null) { + return Observable.Defer(() => + Observable.Create>(async observer => { + Debug.WriteLine($"Create observable thread id: {Thread.CurrentThread.ManagedThreadId}"); + await client.Options.PreprocessRequest(request, client); + var startRequest = new GraphQLWebSocketRequest { + Id = Guid.NewGuid().ToString("N"), + Type = GraphQLWebSocketMessageType.GQL_START, + Payload = request + }; + var closeRequest = new GraphQLWebSocketRequest { + Id = startRequest.Id, + Type = GraphQLWebSocketMessageType.GQL_STOP + }; + var initRequest = new GraphQLWebSocketRequest { + Id = startRequest.Id, + Type = GraphQLWebSocketMessageType.GQL_CONNECTION_INIT, + }; + + var observable = Observable.Create>(o => + IncomingMessageStream + // ignore null values and messages for other requests + .Where(response => response != null && response.Id == startRequest.Id) + .Subscribe(response => { + // terminate the sequence when a 'complete' message is received + if (response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) { + Debug.WriteLine($"received 'complete' message on subscription {startRequest.Id}"); + o.OnCompleted(); + return; + } + + // post the GraphQLResponse to the stream (even if a GraphQL error occurred) + Debug.WriteLine($"received payload on subscription {startRequest.Id} (thread {Thread.CurrentThread.ManagedThreadId})"); + var typedResponse = + client.Options.JsonSerializer.DeserializeToWebsocketResponse( + response.MessageBytes); + o.OnNext(typedResponse.Payload); + + // in case of a GraphQL error, terminate the sequence after the response has been posted + if (response.Type == GraphQLWebSocketMessageType.GQL_ERROR) { + Debug.WriteLine($"terminating subscription {startRequest.Id} because of a GraphQL error"); + o.OnCompleted(); + } + }, + e => { + Debug.WriteLine($"response stream for subscription {startRequest.Id} failed: {e}"); + o.OnError(e); + }, + () => { + Debug.WriteLine($"response stream for subscription {startRequest.Id} completed"); + o.OnCompleted(); + }) + ); + + try { + // initialize websocket (completes immediately if socket is already open) + await InitializeWebSocket(); + } + catch (Exception e) { + // subscribe observer to failed observable + return Observable.Throw>(e).Subscribe(observer); + } + + var disposable = new CompositeDisposable( + observable.Subscribe(observer), + Disposable.Create(async () => { + // only try to send close request on open websocket + if (WebSocketState != WebSocketState.Open) return; + + try { + Debug.WriteLine($"sending close message on subscription {startRequest.Id}"); + await QueueWebSocketRequest(closeRequest); + } + // do not break on disposing + catch (OperationCanceledException) { } + }) + ); + + // send connection init + Debug.WriteLine($"sending connection init on subscription {startRequest.Id}"); + try { + await QueueWebSocketRequest(initRequest); + } + catch (Exception e) { + Console.WriteLine(e); + throw; + } + + Debug.WriteLine($"sending initial message on subscription {startRequest.Id}"); + // send subscription request + try { + await QueueWebSocketRequest(startRequest); + } + catch (Exception e) { + Console.WriteLine(e); + throw; + } + + return disposable; + })) + // complete sequence on OperationCanceledException, this is triggered by the cancellation token + .Catch, OperationCanceledException>(exception => + Observable.Empty>()) + // wrap results + .Select(response => new Tuple, Exception>(response, null)) + // do exception handling + .Catch, Exception>, Exception>(e => { + try { + 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 + if (internalCancellationToken.IsCancellationRequested) + return Observable.Empty, Exception>>(); + else { + Debug.WriteLine($"Catch handler thread id: {Thread.CurrentThread.ManagedThreadId}"); + return Observable.Throw, Exception>>(e); + } + } + catch (Exception exception) { + // wrap all other exceptions to be propagated behind retry + return Observable.Return(new Tuple, Exception>(null, exception)); + } + }) + // attempt to recreate the websocket for rethrown exceptions + .Retry() + // unwrap and push results or throw wrapped exceptions + .SelectMany(t => { + Debug.WriteLine($"unwrap exception thread id: {Thread.CurrentThread.ManagedThreadId}"); + // if the result contains an exception, throw it on the observable + if (t.Item2 != null) + return Observable.Throw>(t.Item2); + + return t.Item1 == null + ? Observable.Empty>() + : Observable.Return(t.Item1); + }) + // transform to hot observable and auto-connect + .Publish().RefCount(); + } + + /// + /// Send a regular GraphQL request (query, mutation) via websocket + /// + /// the response type + /// the to send + /// the token to cancel the request + /// + public Task> SendRequest(GraphQLRequest request, CancellationToken cancellationToken = default) { + return Observable.Create>(async observer => { + await client.Options.PreprocessRequest(request, client); + var websocketRequest = new GraphQLWebSocketRequest { + Id = Guid.NewGuid().ToString("N"), + Type = GraphQLWebSocketMessageType.GQL_START, + Payload = request + }; + var observable = IncomingMessageStream + .Where(response => response != null && response.Id == websocketRequest.Id) + .TakeUntil(response => response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) + .Select(response => { + Debug.WriteLine($"received response for request {websocketRequest.Id}"); + var typedResponse = + client.Options.JsonSerializer.DeserializeToWebsocketResponse( + response.MessageBytes); + return typedResponse.Payload; + }); + + try { + // initialize websocket (completes immediately if socket is already open) + await InitializeWebSocket(); + } + 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 QueueWebSocketRequest(websocketRequest); + } + catch (Exception e) { + Console.WriteLine(e); + throw; + } + + return disposable; + }) + // complete sequence on OperationCanceledException, this is triggered by the cancellation token + .Catch, OperationCanceledException>(exception => + Observable.Empty>()) + .FirstAsync() + .ToTask(cancellationToken); + } + public Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { requestSubject.OnNext(request); return request.SendTask(); @@ -71,7 +309,7 @@ public Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { private async Task SendWebSocketRequest(GraphQLWebSocketRequest request) { try { - if (cancellationToken.IsCancellationRequested) { + if (internalCancellationToken.IsCancellationRequested) { request.SendCanceled(); return; } @@ -82,7 +320,7 @@ await this.clientWebSocket.SendAsync( new ArraySegment(requestBytes), WebSocketMessageType.Text, true, - cancellationToken); + internalCancellationToken); request.SendCompleted(); } catch (Exception e) { @@ -91,9 +329,6 @@ await this.clientWebSocket.SendAsync( } #endregion - - private Task initializeWebSocketTask = Task.CompletedTask; - private readonly object initializeLock = new object(); public Task InitializeWebSocket() { // do not attempt to initialize if cancellation is requested @@ -138,7 +373,7 @@ public Task InitializeWebSocket() { clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; #endif - return initializeWebSocketTask = ConnectAsync(cancellationToken); + return initializeWebSocketTask = ConnectAsync(internalCancellationToken); } } @@ -172,7 +407,7 @@ private Task BackOff() { var delay = Options.BackOffStrategy?.Invoke(connectionAttempt - 1) ?? TimeSpan.FromSeconds(5); Debug.WriteLine($"connection attempt #{connectionAttempt}, backing off for {delay.TotalSeconds} s"); - return Task.Delay(delay, cancellationToken); + return Task.Delay(delay, internalCancellationToken); } @@ -184,7 +419,7 @@ private IObservable GetMessageStream() { } private async Task CreateMessageStream(IObserver observer, CancellationToken token) { - var cts = CancellationTokenSource.CreateLinkedTokenSource(token, cancellationToken); + var cts = CancellationTokenSource.CreateLinkedTokenSource(token, internalCancellationToken); cts.Token.ThrowIfCancellationRequested(); @@ -240,7 +475,7 @@ private async Task CreateMessageStream(IObserver private Task GetReceiveTask() { lock (receiveTaskLocker) { - cancellationToken.ThrowIfCancellationRequested(); + internalCancellationToken.ThrowIfCancellationRequested(); if (receiveAsyncTask == null || receiveAsyncTask.IsFaulted || receiveAsyncTask.IsCompleted) @@ -261,13 +496,13 @@ private async Task ReceiveWebsocketMessagesAsync() { using (var ms = new MemoryStream()) { WebSocketReceiveResult webSocketReceiveResult = null; do { - cancellationToken.ThrowIfCancellationRequested(); + internalCancellationToken.ThrowIfCancellationRequested(); webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); } while (!webSocketReceiveResult.EndOfMessage); - cancellationToken.ThrowIfCancellationRequested(); + internalCancellationToken.ThrowIfCancellationRequested(); ms.Seek(0, SeekOrigin.Begin); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { @@ -325,8 +560,8 @@ public void Complete() { private readonly object completedLocker = new object(); private async Task CompleteAsync() { Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}..."); - if (!cancellationTokenSource.IsCancellationRequested) - cancellationTokenSource.Cancel(); + if (!internalCancellationTokenSource.IsCancellationRequested) + internalCancellationTokenSource.Cancel(); await CloseAsync(); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); @@ -339,7 +574,7 @@ private async Task CompleteAsync() { exceptionSubject?.OnCompleted(); exceptionSubject?.Dispose(); - cancellationTokenSource.Dispose(); + internalCancellationTokenSource.Dispose(); sendLoopScheduler?.Dispose(); receiveLoopScheduler?.Dispose(); diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs deleted file mode 100644 index 82fc6f2d..00000000 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Diagnostics; -using System.Net.WebSockets; -using System.Reactive.Concurrency; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Threading.Tasks; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using GraphQL.Client.Abstractions.Websocket; - -namespace GraphQL.Client.Http.Websocket { - public static class GraphQLHttpWebsocketHelpers { - internal static IObservable> CreateSubscriptionStream( - this GraphQLHttpWebSocket graphQlHttpWebSocket, - GraphQLRequest request, - GraphQLHttpClient client, - Action exceptionHandler = null, - CancellationToken cancellationToken = default) { - return Observable.Defer(() => - Observable.Create>(async observer => { - Debug.WriteLine($"Create observable thread id: {Thread.CurrentThread.ManagedThreadId}"); - await client.Options.PreprocessRequest(request, client); - var startRequest = new GraphQLWebSocketRequest { - Id = Guid.NewGuid().ToString("N"), - Type = GraphQLWebSocketMessageType.GQL_START, - Payload = request - }; - var closeRequest = new GraphQLWebSocketRequest { - Id = startRequest.Id, - Type = GraphQLWebSocketMessageType.GQL_STOP - }; - var initRequest = new GraphQLWebSocketRequest { - Id = startRequest.Id, - Type = GraphQLWebSocketMessageType.GQL_CONNECTION_INIT, - }; - - var observable = Observable.Create>(o => - graphQlHttpWebSocket.ResponseStream - // ignore null values and messages for other requests - .Where(response => response != null && response.Id == startRequest.Id) - .Subscribe(response => { - // terminate the sequence when a 'complete' message is received - if (response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) { - Debug.WriteLine($"received 'complete' message on subscription {startRequest.Id}"); - o.OnCompleted(); - return; - } - - // post the GraphQLResponse to the stream (even if a GraphQL error occurred) - Debug.WriteLine($"received payload on subscription {startRequest.Id} (thread {Thread.CurrentThread.ManagedThreadId})"); - var typedResponse = - client.Options.JsonSerializer.DeserializeToWebsocketResponse( - response.MessageBytes); - o.OnNext(typedResponse.Payload); - - // in case of a GraphQL error, terminate the sequence after the response has been posted - if (response.Type == GraphQLWebSocketMessageType.GQL_ERROR) { - Debug.WriteLine($"terminating subscription {startRequest.Id} because of a GraphQL error"); - o.OnCompleted(); - } - }, - e => { - Debug.WriteLine($"response stream for subscription {startRequest.Id} failed: {e}"); - o.OnError(e); - }, - () => { - Debug.WriteLine($"response stream for subscription {startRequest.Id} completed"); - o.OnCompleted(); - }) - ); - - try { - // initialize websocket (completes immediately if socket is already open) - await graphQlHttpWebSocket.InitializeWebSocket(); - } - catch (Exception e) { - // subscribe observer to failed observable - return Observable.Throw>(e).Subscribe(observer); - } - - var disposable = new CompositeDisposable( - observable.Subscribe(observer), - Disposable.Create(async () => { - // only try to send close request on open websocket - if (graphQlHttpWebSocket.WebSocketState != WebSocketState.Open) return; - - try { - Debug.WriteLine($"sending close message on subscription {startRequest.Id}"); - await graphQlHttpWebSocket.QueueWebSocketRequest(closeRequest); - } - // do not break on disposing - catch (OperationCanceledException) { } - }) - ); - - // send connection init - Debug.WriteLine($"sending connection init on subscription {startRequest.Id}"); - try { - await graphQlHttpWebSocket.QueueWebSocketRequest(initRequest); - } - catch (Exception e) { - Console.WriteLine(e); - throw; - } - - Debug.WriteLine($"sending initial message on subscription {startRequest.Id}"); - // send subscription request - try { - await graphQlHttpWebSocket.QueueWebSocketRequest(startRequest); - } - catch (Exception e) { - Console.WriteLine(e); - throw; - } - - return disposable; - })) - // complete sequence on OperationCanceledException, this is triggered by the cancellation token - .Catch, OperationCanceledException>(exception => - Observable.Empty>()) - // wrap results - .Select(response => new Tuple, Exception>(response, null)) - // do exception handling - .Catch, Exception>, Exception>(e => { - try { - 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 - if (cancellationToken.IsCancellationRequested) - return Observable.Empty, Exception>>(); - else { - Debug.WriteLine($"Catch handler thread id: {Thread.CurrentThread.ManagedThreadId}"); - return Observable.Throw, Exception>>(e); - } - } - catch (Exception exception) { - // wrap all other exceptions to be propagated behind retry - return Observable.Return(new Tuple, Exception>(null, exception)); - } - }) - // attempt to recreate the websocket for rethrown exceptions - .Retry() - // unwrap and push results or throw wrapped exceptions - .SelectMany(t => { - Debug.WriteLine($"unwrap exception thread id: {Thread.CurrentThread.ManagedThreadId}"); - // if the result contains an exception, throw it on the observable - if (t.Item2 != null) - return Observable.Throw>(t.Item2); - - return t.Item1 == null - ? Observable.Empty>() - : Observable.Return(t.Item1); - }) - // transform to hot observable and auto-connect - .Publish().RefCount(); - } - - internal static Task> SendRequest( - this GraphQLHttpWebSocket graphQlHttpWebSocket, - GraphQLRequest request, - GraphQLHttpClient client, - CancellationToken cancellationToken = default) { - return Observable.Create>(async observer => { - await client.Options.PreprocessRequest(request, client); - var websocketRequest = new GraphQLWebSocketRequest { - Id = Guid.NewGuid().ToString("N"), - Type = GraphQLWebSocketMessageType.GQL_START, - Payload = request - }; - var observable = graphQlHttpWebSocket.ResponseStream - .Where(response => response != null && response.Id == websocketRequest.Id) - .TakeUntil(response => response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) - .Select(response => { - Debug.WriteLine($"received response for request {websocketRequest.Id}"); - var typedResponse = - client.Options.JsonSerializer.DeserializeToWebsocketResponse( - response.MessageBytes); - return typedResponse.Payload; - }); - - try { - // intialize websocket (completes immediately if socket is already open) - await graphQlHttpWebSocket.InitializeWebSocket(); - } - 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.QueueWebSocketRequest(websocketRequest); - } - catch (Exception e) { - Console.WriteLine(e); - throw; - } - - return disposable; - }) - // complete sequence on OperationCanceledException, this is triggered by the cancellation token - .Catch, OperationCanceledException>(exception => - Observable.Empty>()) - .FirstAsync() - .ToTask(cancellationToken); - } - } -} From b1888b4931c0c964d5129e771a8b4c74ae0f7617 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 13:00:00 +0100 Subject: [PATCH 16/24] eliminate incomingMessagesSubject --- .../Websocket/GraphQLHttpWebSocket.cs | 151 +++++++++--------- .../Helpers/ObservableTester.cs | 7 +- .../WebsocketTests/Base.cs | 9 +- 3 files changed, 83 insertions(+), 84 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 61462a10..a8efeaeb 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -31,8 +31,8 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly EventLoopScheduler sendLoopScheduler = new EventLoopScheduler(); private int connectionAttempt = 0; - private Subject incomingMessagesSubject; - private IDisposable incomingMessagesDisposable; + private IConnectableObservable incomingMessages; + private IDisposable incomingMessagesConnection; private GraphQLHttpClientOptions Options => client.Options; private Task initializeWebSocketTask = Task.CompletedTask; @@ -302,7 +302,7 @@ public Task> SendRequest(GraphQLRequest re .ToTask(cancellationToken); } - public Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { + private Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { requestSubject.OnNext(request); return request.SendTask(); } @@ -381,13 +381,41 @@ private async Task ConnectAsync(CancellationToken token) { try { await BackOff(); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting); - Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); + Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})"); await clientWebSocket.ConnectAsync(webSocketUri, token); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected); Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}, invoking Options.OnWebsocketConnected()"); await (Options.OnWebsocketConnected?.Invoke(client) ?? Task.CompletedTask); Debug.WriteLine($"invoking Options.OnWebsocketConnected() on websocket {clientWebSocket.GetHashCode()}"); connectionAttempt = 1; + + // create receiving observable + incomingMessages = Observable + .Defer(() => GetReceiveTask().ToObservable().ObserveOn(receiveLoopScheduler)) + .Repeat() + // complete sequence on OperationCanceledException, this is triggered by the cancellation token on disposal + .Catch(exception => Observable.Empty()) + .Publish(); + + // subscribe maintenance + var maintenanceSubscription = incomingMessages.Subscribe(_ => { }, ex => { + Debug.WriteLine($"incoming message stream {incomingMessages.GetHashCode()} received an error: {ex}"); + exceptionSubject.OnNext(ex); + incomingMessagesConnection?.Dispose(); + stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); + }, + () => { + Debug.WriteLine($"incoming message stream {incomingMessages.GetHashCode()} completed"); + incomingMessagesConnection?.Dispose(); + stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); + }); + + + // connect observable + var connection = incomingMessages.Connect(); + Debug.WriteLine($"new incoming message stream {incomingMessages.GetHashCode()} created"); + + incomingMessagesConnection = new CompositeDisposable(maintenanceSubscription, connection); } catch (Exception e) { stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); @@ -410,61 +438,25 @@ private Task BackOff() { return Task.Delay(delay, internalCancellationToken); } - private IObservable GetMessageStream() { - return Observable.Create(CreateMessageStream) - // complete sequence on OperationCanceledException, this is triggered by the cancellation token on disposal - .Catch(exception => - Observable.Empty()); - } - - private async Task CreateMessageStream(IObserver observer, CancellationToken token) { - var cts = CancellationTokenSource.CreateLinkedTokenSource(token, internalCancellationToken); - cts.Token.ThrowIfCancellationRequested(); - - - if (incomingMessagesSubject == null || incomingMessagesSubject.IsDisposed) { - // create new response subject - incomingMessagesSubject = new Subject(); - Debug.WriteLine($"creating new incoming message stream {incomingMessagesSubject.GetHashCode()}"); - - // initialize and connect websocket - await InitializeWebSocket(); - - // loop the receive task and subscribe the created subject to the results - var receiveLoopSubscription = Observable - .Defer(() => GetReceiveTask().ToObservable()) - .Repeat() - .Subscribe(incomingMessagesSubject); - - incomingMessagesDisposable = new CompositeDisposable( - incomingMessagesSubject, - receiveLoopSubscription, - Disposable.Create(() => { - Debug.WriteLine($"incoming message stream {incomingMessagesSubject.GetHashCode()} disposed"); + return Observable.Using(() => new EventLoopScheduler(), scheduler => + Observable.Create(async observer => { + // make sure the websocket ist connected + await InitializeWebSocket(); + // subscribe observer to message stream + var subscription = new CompositeDisposable(incomingMessages.ObserveOn(scheduler).Subscribe(observer)); + // register the observer's OnCompleted method with the cancellation token to complete the sequence on disposal + subscription.Add(internalCancellationTokenSource.Token.Register(observer.OnCompleted)); + + // add some debug output + var hashCode = subscription.GetHashCode(); + subscription.Add(Disposable.Create(() => { + Debug.WriteLine($"incoming message subscription {hashCode} disposed"); })); + Debug.WriteLine($"new incoming message subscription {hashCode} created"); - // dispose the subject on any error or completion (will be recreated) - incomingMessagesSubject.Subscribe(_ => { }, ex => { - exceptionSubject.OnNext(ex); - incomingMessagesDisposable?.Dispose(); - incomingMessagesSubject = null; - stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); - }, - () => { - incomingMessagesDisposable?.Dispose(); - incomingMessagesSubject = null; - stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); - }); - } - - var subscription = new CompositeDisposable(incomingMessagesSubject.Subscribe(observer)); - var hashCode = subscription.GetHashCode(); - subscription.Add(Disposable.Create(() => { - Debug.WriteLine($"incoming message subscription {hashCode} disposed"); - })); - Debug.WriteLine($"new incoming message subscription {hashCode} created"); - return subscription; + return subscription; + })); } private Task receiveAsyncTask = null; @@ -490,30 +482,31 @@ private Task GetReceiveTask() { /// /// private async Task ReceiveWebsocketMessagesAsync() { + internalCancellationToken.ThrowIfCancellationRequested(); + try { Debug.WriteLine($"waiting for data on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); - using (var ms = new MemoryStream()) { - WebSocketReceiveResult webSocketReceiveResult = null; - do { - internalCancellationToken.ThrowIfCancellationRequested(); - webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); - ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); - } - while (!webSocketReceiveResult.EndOfMessage); + using var ms = new MemoryStream(); + WebSocketReceiveResult webSocketReceiveResult = null; + do { + // cancellation is done implicitly via the close method + webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); + ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); + } + while (!webSocketReceiveResult.EndOfMessage && !internalCancellationToken.IsCancellationRequested); - internalCancellationToken.ThrowIfCancellationRequested(); - ms.Seek(0, SeekOrigin.Begin); + internalCancellationToken.ThrowIfCancellationRequested(); + ms.Seek(0, SeekOrigin.Begin); - if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { - var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); - response.MessageBytes = ms.ToArray(); - Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); - return response; - } - else { - throw new NotSupportedException("binary websocket messages are not supported"); - } + if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { + var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); + response.MessageBytes = ms.ToArray(); + Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); + return response; + } + else { + throw new NotSupportedException("binary websocket messages are not supported"); } } catch (Exception e) { @@ -560,15 +553,15 @@ public void Complete() { private readonly object completedLocker = new object(); private async Task CompleteAsync() { Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}..."); + incomingMessagesConnection?.Dispose(); + if (!internalCancellationTokenSource.IsCancellationRequested) internalCancellationTokenSource.Cancel(); + await CloseAsync(); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); - - incomingMessagesSubject?.OnCompleted(); - incomingMessagesDisposable?.Dispose(); - + stateSubject?.OnCompleted(); stateSubject?.Dispose(); diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 86ec3a1f..bd1ac46b 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -89,7 +89,12 @@ public AndWhichConstraint, TPayload> HaveReceiv string because = "", params object[] becauseArgs) { Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => Subject.updateReceived.Wait(timeout)) + .Given(() => { + var isSet = Subject.updateReceived.Wait(timeout); + if(!isSet) + Debug.WriteLine($"waiting for payload on thread {Thread.CurrentThread.ManagedThreadId} timed out!"); + return isSet; + }) .ForCondition(isSet => isSet) .FailWith("Expected {context:Subscription} to receive new payload{reason}, but did not receive an update within {0}", timeout); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 47d2e5e8..e689dd06 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -169,24 +169,25 @@ public async void CanReconnectWithSameObservable() { const string message1 = "Hello World"; Debug.WriteLine($"adding message {message1}"); - var response = await ChatClient.AddMessageAsync(message1); + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(true); response.Data.AddMessage.Content.Should().Be(message1); tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "How are you?"; - response = await ChatClient.AddMessageAsync(message2); + response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(true); response.Data.AddMessage.Content.Should().Be(message2); tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); Debug.WriteLine("disposing subscription..."); tester.Dispose(); // does not close the websocket connection - Debug.WriteLine("creating new subscription..."); + Debug.WriteLine($"creating new subscription from thread {Thread.CurrentThread.ManagedThreadId} ..."); var tester2 = observable.Monitor(); + Debug.WriteLine($"waiting for payload on {Thread.CurrentThread.ManagedThreadId} ..."); tester2.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); const string message3 = "lorem ipsum dolor si amet"; - response = await ChatClient.AddMessageAsync(message3); + response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(true); response.Data.AddMessage.Content.Should().Be(message3); tester2.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message3); From c9acd489f0a565378c8a1f77d97c2178df1e0fd5 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 13:33:46 +0100 Subject: [PATCH 17/24] fix tests locally --- .../Websocket/GraphQLHttpWebSocket.cs | 2 +- .../Helpers/ObservableTester.cs | 6 +----- .../WebsocketTests/Base.cs | 18 ++++++++++-------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index a8efeaeb..ec3d5a18 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -502,7 +502,7 @@ private async Task ReceiveWebsocketMessagesAsync() { if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); response.MessageBytes = ms.ToArray(); - Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); + Debug.WriteLine($"{response.MessageBytes.Length} bytes received for id {response.Id} on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); return response; } else { diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index bd1ac46b..05ed7e38 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -13,7 +13,6 @@ public class ObservableTester : IDisposable { private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); private readonly ManualResetEventSlim error = new ManualResetEventSlim(); - private readonly EventLoopScheduler subscriptionScheduler = new EventLoopScheduler(); private readonly EventLoopScheduler observeScheduler = new EventLoopScheduler(); /// @@ -37,13 +36,11 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - subscriptionScheduler.Schedule(() => - Debug.WriteLine($"Subscription scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); observeScheduler.Schedule(() => Debug.WriteLine($"Observe scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); - subscription = observable.SubscribeOn(subscriptionScheduler).ObserveOn(observeScheduler).Subscribe( + subscription = observable.ObserveOn(observeScheduler).Subscribe( obj => { Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; @@ -70,7 +67,6 @@ private void Reset() { /// public void Dispose() { subscription?.Dispose(); - subscriptionScheduler.Dispose(); observeScheduler.Dispose(); } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index e689dd06..3c2d63f1 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -233,36 +233,38 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { var observable2 = ChatClient.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Invoke); Debug.WriteLine("subscribing..."); - var tester = observable1.Monitor(); - var tester2 = observable2.Monitor(); + var messagesMonitor = observable1.Monitor(); + var joinedMonitor = observable2.Monitor(); - tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + messagesMonitor.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); const string message1 = "Hello World"; var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload() + messagesMonitor.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message1); + joinedMonitor.Should().NotHaveReceivedPayload(); var joinResponse = await ChatClient.JoinDeveloperUser(); joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); - var payload = tester2.Should().HaveReceivedPayload().Subject; + var payload = joinedMonitor.Should().HaveReceivedPayload().Subject; payload.Data.UserJoined.Id.Should().Be("1", "because that's the id we sent with our mutation request"); payload.Data.UserJoined.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); + messagesMonitor.Should().NotHaveReceivedPayload(); Debug.WriteLine("disposing subscription..."); - tester2.Dispose(); + joinedMonitor.Dispose(); const string message3 = "lorem ipsum dolor si amet"; response = await ChatClient.AddMessageAsync(message3); response.Data.AddMessage.Content.Should().Be(message3); - tester.Should().HaveReceivedPayload() + messagesMonitor.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message3); // disposing the client should complete the subscription ChatClient.Dispose(); - tester.Should().HaveCompleted(); + messagesMonitor.Should().HaveCompleted(); } From 3a9df0ca08a1257a51e87b18677f6b1072380df5 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 13:58:50 +0100 Subject: [PATCH 18/24] extend execution time in cancellation tests --- .../QueryAndMutationTests/Base.cs | 4 ++-- tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index c25117ca..7b2dc79e 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -171,7 +171,7 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(500.Milliseconds()); + request.Invoking().ExecutionTime().Should().BeLessThan(1000.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -183,7 +183,7 @@ query Long { chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(500.Milliseconds()); + .ExecutionTime().Should().BeLessThan(1000.Milliseconds()); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 3c2d63f1..016f0dab 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net.WebSockets; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; @@ -77,11 +76,11 @@ query Long { // start request request.Start(); // wait until the query has reached the server - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(500.Milliseconds()); + request.Invoking().ExecutionTime().Should().BeLessThan(1000.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -90,10 +89,10 @@ query Long { // cancellation test request.Start(); - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(500.Milliseconds()); + .ExecutionTime().Should().BeLessThan(1000.Milliseconds()); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); From 6834eef0a4b23d53f283da1fb4012a4c6e48e8af Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 14:27:26 +0100 Subject: [PATCH 19/24] don't measure execution time in tests --- tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs | 4 +--- tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 7b2dc79e..e60c26f0 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -171,7 +171,6 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(1000.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -182,8 +181,7 @@ query Long { request.Start(); chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); - FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(1000.Milliseconds()); + request.Invoking().Should().Throw("because the request was cancelled"); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 016f0dab..59f57ceb 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -80,7 +80,6 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(1000.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -91,8 +90,7 @@ query Long { request.Start(); chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); - FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(1000.Milliseconds()); + request.Invoking().Should().Throw("because the request was cancelled"); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); From 80ec16795fe588e101f58e2e8764fee084e65854 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 14:35:48 +0100 Subject: [PATCH 20/24] enable windows workflow for comparison --- .github/workflows/branches-windows.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/branches-windows.yml b/.github/workflows/branches-windows.yml index 430b2121..a896d523 100644 --- a/.github/workflows/branches-windows.yml +++ b/.github/workflows/branches-windows.yml @@ -1,8 +1,10 @@ name: Branch workflow -on: +on: push: branches-ignore: - - '**' + - develop + - 'release/**' + - 'releases/**' jobs: generateVersionInfo: name: GenerateVersionInfo From 0a7f91d03ffeecd162e3a3177d1dacb45faf96a7 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 15:09:38 +0100 Subject: [PATCH 21/24] disable windows job again --- .github/workflows/branches-windows.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/branches-windows.yml b/.github/workflows/branches-windows.yml index a896d523..fdf2b81c 100644 --- a/.github/workflows/branches-windows.yml +++ b/.github/workflows/branches-windows.yml @@ -2,9 +2,7 @@ name: Branch workflow on: push: branches-ignore: - - develop - - 'release/**' - - 'releases/**' + - '**' jobs: generateVersionInfo: name: GenerateVersionInfo From 52c8731e1f4941d94664d20f611a29ea5a8c4730 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 17:10:02 +0100 Subject: [PATCH 22/24] use a Dictionary as a base for GraphQLRequest --- .../GraphQLWebSocketRequest.cs | 20 ++- .../GraphQLRequest.cs | 21 --- .../GraphQLWebSocketRequest.cs | 23 --- .../NewtonsoftJsonSerializer.cs | 4 +- .../GraphQLRequest.cs | 21 --- .../GraphQLWebSocketRequest.cs | 23 --- .../ImmutableConverter.cs | 166 ++++++++++++++++++ .../JsonSerializerOptionsExtensions.cs | 22 +++ .../SystemTextJsonSerializer.cs | 8 +- src/GraphQL.Primitives/GraphQLRequest.cs | 20 ++- .../BaseSerializerTest.cs | 3 +- .../NewtonsoftSerializerTest.cs | 1 + .../SystemTextJsonSerializerTests.cs | 2 +- .../TestData/SerializeToBytesTestData.cs | 4 +- .../TestData/SerializeToStringTestData.cs | 4 +- 15 files changed, 227 insertions(+), 115 deletions(-) delete mode 100644 src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs delete mode 100644 src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs delete mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs delete mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs create mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/ImmutableConverter.cs create mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs diff --git a/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs index 0f486482..a44e57d6 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs @@ -8,7 +8,7 @@ namespace GraphQL.Client.Abstractions.Websocket { /// /// A Subscription Request /// - public class GraphQLWebSocketRequest : IEquatable { + public class GraphQLWebSocketRequest : Dictionary, IEquatable { public const string IdKey = "id"; public const string TypeKey = "type"; public const string PayloadKey = "payload"; @@ -16,20 +16,26 @@ public class GraphQLWebSocketRequest : IEquatable { /// /// The Identifier of the Response /// - [DataMember(Name = IdKey)] - public virtual string Id { get; set; } + public string Id { + get => ContainsKey(IdKey) ? (string)this[IdKey] : null; + set => this[IdKey] = value; + } /// /// The Type of the Request /// - [DataMember(Name = TypeKey)] - public virtual string Type { get; set; } + public string Type { + get => ContainsKey(TypeKey) ? (string)this[TypeKey] : null; + set => this[TypeKey] = value; + } /// /// The payload of the websocket request /// - [DataMember(Name = PayloadKey)] - public virtual GraphQLRequest Payload { get; set; } + public GraphQLRequest Payload { + get => ContainsKey(PayloadKey) ? (GraphQLRequest) this[PayloadKey] : null; + set => this[PayloadKey] = value; + } private TaskCompletionSource _tcs = new TaskCompletionSource(); diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs deleted file mode 100644 index cf012c92..00000000 --- a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace GraphQL.Client.Serializer.Newtonsoft { - public class GraphQLRequest: GraphQL.GraphQLRequest { - [JsonProperty(QueryKey)] - public override string Query { get; set; } - [JsonProperty(OperationNameKey)] - public override string? OperationName { get; set; } - [JsonProperty(VariablesKey)] - public override object? Variables { get; set; } - - public GraphQLRequest() { } - - public GraphQLRequest(GraphQL.GraphQLRequest other) { - Query = other.Query; - OperationName = other.OperationName; - Variables = other.Variables; - } - - } -} diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs deleted file mode 100644 index c575b9d3..00000000 --- a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace GraphQL.Client.Serializer.Newtonsoft { - public class GraphQLWebSocketRequest: Abstractions.Websocket.GraphQLWebSocketRequest { - - [JsonProperty(IdKey)] - public override string Id { get; set; } - [JsonProperty(TypeKey)] - public override string Type { get; set; } - [JsonProperty(PayloadKey)] - public override GraphQL.GraphQLRequest Payload { get; set; } - - public GraphQLWebSocketRequest() - { - } - - public GraphQLWebSocketRequest(Abstractions.Websocket.GraphQLWebSocketRequest other) { - Id = other.Id; - Type = other.Type; - Payload = other.Payload != null ? new GraphQLRequest(other.Payload) : null; // create serializer-specific type - } - } -} diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index 7194a547..67679432 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -34,11 +34,11 @@ private void ConfigureMandatorySerializerOptions() { } public string SerializeToString(GraphQL.GraphQLRequest request) { - return JsonConvert.SerializeObject(new GraphQLRequest(request), JsonSerializerSettings); + return JsonConvert.SerializeObject(request, JsonSerializerSettings); } public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest request) { - var json = JsonConvert.SerializeObject(new GraphQLWebSocketRequest(request), JsonSerializerSettings); + var json = JsonConvert.SerializeObject(request, JsonSerializerSettings); return Encoding.UTF8.GetBytes(json); } diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs deleted file mode 100644 index 1f3b7908..00000000 --- a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace GraphQL.Client.Serializer.SystemTextJson { - public class GraphQLRequest: GraphQL.GraphQLRequest { - [JsonPropertyName(QueryKey)] - public override string Query { get; set; } - [JsonPropertyName(OperationNameKey)] - public override string? OperationName { get; set; } - [JsonPropertyName(VariablesKey)] - public override object? Variables { get; set; } - - public GraphQLRequest() { } - - public GraphQLRequest(GraphQL.GraphQLRequest other) { - Query = other.Query; - OperationName = other.OperationName; - Variables = other.Variables; - } - - } -} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs deleted file mode 100644 index 4961e00d..00000000 --- a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Text.Json.Serialization; - -namespace GraphQL.Client.Serializer.SystemTextJson { - public class GraphQLWebSocketRequest: Abstractions.Websocket.GraphQLWebSocketRequest { - - [JsonPropertyName(IdKey)] - public override string Id { get; set; } - [JsonPropertyName(TypeKey)] - public override string Type { get; set; } - [JsonPropertyName(PayloadKey)] - public override GraphQL.GraphQLRequest Payload { get; set; } - - public GraphQLWebSocketRequest() - { - } - - public GraphQLWebSocketRequest(Abstractions.Websocket.GraphQLWebSocketRequest other) { - Id = other.Id; - Type = other.Type; - Payload = other.Payload != null ? new GraphQLRequest(other.Payload) : null; // create serializer-specific type; - } - } -} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/ImmutableConverter.cs b/src/GraphQL.Client.Serializer.SystemTextJson/ImmutableConverter.cs new file mode 100644 index 00000000..a4f49f49 --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/ImmutableConverter.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GraphQL.Client.Serializer.SystemTextJson { + + /// + /// class for converting immutable objects, derived from https://github.com/manne/obviously/blob/master/src/system.text.json/Core/ImmutableConverter.cs + /// + public class ImmutableConverter : JsonConverter { + public override bool CanConvert(Type typeToConvert) { + bool result; + var constructors = typeToConvert.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + if (constructors.Length != 1) { + result = false; + } + else { + var constructor = constructors[0]; + var parameters = constructor.GetParameters(); + var hasParameters = parameters.Length > 0; + if (hasParameters) { + var properties = typeToConvert.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + result = true; + foreach (var parameter in parameters) { + var hasMatchingProperty = properties.Any(p => + NameOfPropertyAndParameter.Matches(p.Name, parameter.Name, typeToConvert.IsAnonymous())); + if (!hasMatchingProperty) { + result = false; + break; + } + } + } + else { + result = false; + } + } + + return result; + } + + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + var valueOfProperty = new Dictionary(); + var namedPropertiesMapping = GetNamedProperties(options, GetProperties(typeToConvert)); + reader.Read(); + while (true) { + if (reader.TokenType != JsonTokenType.PropertyName && reader.TokenType != JsonTokenType.String) { + break; + } + + var jsonPropName = reader.GetString(); + var normalizedPropName = ConvertAndNormalizeName(jsonPropName, options); + if (!namedPropertiesMapping.TryGetValue(normalizedPropName, out var obProp)) { + reader.Read(); + } + else { + var value = JsonSerializer.Deserialize(ref reader, obProp.PropertyType, options); + reader.Read(); + valueOfProperty[obProp] = value; + } + } + + var ctor = typeToConvert.GetConstructors(BindingFlags.Public | BindingFlags.Instance).First(); + var parameters = ctor.GetParameters(); + var parameterValues = new object[parameters.Length]; + for (var index = 0; index < parameters.Length; index++) { + var parameterInfo = parameters[index]; + var value = valueOfProperty.First(prop => + NameOfPropertyAndParameter.Matches(prop.Key.Name, parameterInfo.Name, typeToConvert.IsAnonymous())).Value; + + parameterValues[index] = value; + } + + var instance = ctor.Invoke(parameterValues); + return instance; + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { + var strippedOptions = new JsonSerializerOptions { + AllowTrailingCommas = options.AllowTrailingCommas, + DefaultBufferSize = options.DefaultBufferSize, + DictionaryKeyPolicy = options.DictionaryKeyPolicy, + Encoder = options.Encoder, + IgnoreNullValues = options.IgnoreNullValues, + IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties, + MaxDepth = options.MaxDepth, + PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive, + PropertyNamingPolicy = options.PropertyNamingPolicy, + ReadCommentHandling = options.ReadCommentHandling, + WriteIndented = options.WriteIndented + }; + foreach (var converter in options.Converters) { + if (!(converter is ImmutableConverter)) + strippedOptions.Converters.Add(converter); + } + JsonSerializer.Serialize(writer, value, strippedOptions); + } + + private static PropertyInfo[] GetProperties(IReflect typeToConvert) { + return typeToConvert.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + } + + private static IReadOnlyDictionary GetNamedProperties(JsonSerializerOptions options, IEnumerable properties) { + var result = new Dictionary(); + foreach (var property in properties) { + string name; + var nameAttribute = property.GetCustomAttribute(); + if (nameAttribute != null) { + name = nameAttribute.Name; + } + else { + name = options.PropertyNamingPolicy?.ConvertName(property.Name) ?? property.Name; + } + + var normalizedName = NormalizeName(name, options); + result.Add(normalizedName, property); + } + + return result; + } + + private static string ConvertAndNormalizeName(string name, JsonSerializerOptions options) { + var convertedName = options.PropertyNamingPolicy?.ConvertName(name) ?? name; + return options.PropertyNameCaseInsensitive ? convertedName.ToLowerInvariant() : convertedName; + } + + private static string NormalizeName(string name, JsonSerializerOptions options) { + return options.PropertyNameCaseInsensitive ? name.ToLowerInvariant() : name; + } + } + + internal static class NameOfPropertyAndParameter { + public static bool Matches(string propertyName, string parameterName, bool anonymousType) { + if (propertyName is null && parameterName is null) { + return true; + } + + if (propertyName is null || parameterName is null) { + return false; + } + + if (anonymousType) { + return propertyName.Equals(parameterName, StringComparison.Ordinal); + } + else { + var xRight = propertyName.AsSpan(1); + var yRight = parameterName.AsSpan(1); + return char.ToLowerInvariant(propertyName[0]).CompareTo(parameterName[0]) == 0 && xRight.Equals(yRight, StringComparison.Ordinal); + } + } + } + + internal static class TypeExtensions { + // copied from https://github.com/dahomey-technologies/Dahomey.Json/blob/master/src/Dahomey.Json/Util/TypeExtensions.cs + public static bool IsAnonymous(this Type type) { + return type.Namespace == null + && type.IsSealed + && type.BaseType == typeof(object) + && !type.IsPublic + && type.IsDefined(typeof(CompilerGeneratedAttribute), false); + } + } +} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs b/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs new file mode 100644 index 00000000..81547955 --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dahomey.Json; +using Dahomey.Json.Serialization.Converters.Factories; + +namespace GraphQL.Client.Serializer.SystemTextJson { + public static class JsonSerializerOptionsExtensions { + public static JsonSerializerOptions SetupDahomeyExtensions( + this JsonSerializerOptions options) { + options.Converters.Add(new ImmutableConverter()); + //options.Converters.Add((JsonConverter)new JsonSerializerOptionsState(options)); + //options.Converters.Add((JsonConverter)new DictionaryConverterFactory()); + //options.Converters.Add((JsonConverter)new CollectionConverterFactory()); + //options.Converters.Add((JsonConverter)new JsonNodeConverterFactory()); + //options.Converters.Add((JsonConverter)new ObjectConverterFactory()); + return options; + } + } +} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index 62be2e37..a00c834b 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -13,8 +13,8 @@ public class SystemTextJsonSerializer: IGraphQLWebsocketJsonSerializer { public static JsonSerializerOptions DefaultJsonSerializerOptions => new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }.SetupExtensions(); - + }.SetupDahomeyExtensions(); + public JsonSerializerOptions Options { get; } public SystemTextJsonSerializer() : this(DefaultJsonSerializerOptions) { } @@ -34,7 +34,7 @@ private void ConfigureMandatorySerializerOptions() { } public string SerializeToString(GraphQL.GraphQLRequest request) { - return JsonSerializer.Serialize(new GraphQLRequest(request), Options); + return JsonSerializer.Serialize(request, Options); } public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { @@ -42,7 +42,7 @@ public Task> DeserializeFromUtf8StreamAsync DeserializeToWebsocketResponseWrapperAsync(Stream stream) { diff --git a/src/GraphQL.Primitives/GraphQLRequest.cs b/src/GraphQL.Primitives/GraphQLRequest.cs index f7e48e24..ae174392 100644 --- a/src/GraphQL.Primitives/GraphQLRequest.cs +++ b/src/GraphQL.Primitives/GraphQLRequest.cs @@ -7,7 +7,7 @@ namespace GraphQL { /// /// A GraphQL request /// - public class GraphQLRequest : IEquatable { + public class GraphQLRequest : Dictionary, IEquatable { public const string OperationNameKey = "operationName"; public const string QueryKey = "query"; public const string VariablesKey = "variables"; @@ -15,20 +15,26 @@ public class GraphQLRequest : IEquatable { /// /// The Query /// - [DataMember(Name = QueryKey)] - public virtual string Query { get; set; } + public string Query { + get => ContainsKey(QueryKey) ? (string) this[QueryKey] : null; + set => this[QueryKey] = value; + } /// /// The name of the Operation /// - [DataMember(Name = OperationNameKey)] - public virtual string? OperationName { get; set; } + public string? OperationName { + get => ContainsKey(OperationNameKey) ? (string)this[OperationNameKey] : null; + set => this[OperationNameKey] = value; + } /// /// Represents the request variables /// - [DataMember(Name = VariablesKey)] - public virtual object? Variables { get; set; } + public object? Variables { + get => ContainsKey(VariablesKey) ? this[VariablesKey] : null; + set => this[VariablesKey] = value; + } public GraphQLRequest() { } diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs index 579e756e..26d44472 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs @@ -75,8 +75,7 @@ query Droid($id: String!) { new { id = id.ToString() }, "Human"); - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - ; + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); diff --git a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs index 84bb5e84..95c18908 100644 --- a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using GraphQL.Client.Serializer.Newtonsoft; using Newtonsoft.Json; diff --git a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs index a3d393dc..212aefa6 100644 --- a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs +++ b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs @@ -9,6 +9,6 @@ public SystemTextJsonSerializerTests() : base(new SystemTextJsonSerializer()) { public class SystemTextJsonSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest { public SystemTextJsonSerializeNoCamelCaseTest() - : base(new SystemTextJsonSerializer(new JsonSerializerOptions().SetupExtensions())) { } + : base(new SystemTextJsonSerializer(new JsonSerializerOptions().SetupDahomeyExtensions())) { } } } diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs index 41d15ce0..a506cb6d 100644 --- a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs @@ -7,7 +7,7 @@ namespace GraphQL.Client.Serializer.Tests.TestData { public class SerializeToBytesTestData : IEnumerable { public IEnumerator GetEnumerator() { yield return new object[] { - "{\"id\":\"1234567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":null}}", + "{\"id\":\"1234567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null}}", new GraphQLWebSocketRequest { Id = "1234567", Type = GraphQLWebSocketMessageType.GQL_START, @@ -15,7 +15,7 @@ public IEnumerator GetEnumerator() { } }; yield return new object[] { - "{\"id\":\"34476567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"}}}", + "{\"id\":\"34476567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"},\"operationName\":null}}", new GraphQLWebSocketRequest { Id = "34476567", Type = GraphQLWebSocketMessageType.GQL_START, diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs index 86d1ae9f..4f9d694c 100644 --- a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs @@ -6,11 +6,11 @@ namespace GraphQL.Client.Serializer.Tests.TestData { public class SerializeToStringTestData : IEnumerable { public IEnumerator GetEnumerator() { yield return new object[] { - "{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":null}", + "{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null}", new GraphQLRequest("simple query string") }; yield return new object[] { - "{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"}}", + "{\"query\":\"simplequerystring\",\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"},\"operationName\":null}", new GraphQLRequest("simple query string", new { camelCaseProperty = "camelCase", PascalCaseProperty = "PascalCase"}) }; } From 86424d5c295adefc2b9f6b1e8ffaa71b269e68a8 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 17:16:45 +0100 Subject: [PATCH 23/24] remove dependency on Dahomey.Json, add test with extension --- .../GraphQL.Client.Serializer.SystemTextJson.csproj | 4 ---- .../JsonSerializerOptionsExtensions.cs | 13 +------------ .../SystemTextJsonSerializer.cs | 3 +-- .../SystemTextJsonSerializerTests.cs | 3 +-- .../TestData/SerializeToStringTestData.cs | 4 ++++ 5 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj index 024b0681..8f8bc66a 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj +++ b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj @@ -10,10 +10,6 @@ - - - - diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs b/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs index 81547955..b4cc2614 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs @@ -1,21 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; -using Dahomey.Json; -using Dahomey.Json.Serialization.Converters.Factories; namespace GraphQL.Client.Serializer.SystemTextJson { public static class JsonSerializerOptionsExtensions { - public static JsonSerializerOptions SetupDahomeyExtensions( + public static JsonSerializerOptions SetupImmutableConverter( this JsonSerializerOptions options) { options.Converters.Add(new ImmutableConverter()); - //options.Converters.Add((JsonConverter)new JsonSerializerOptionsState(options)); - //options.Converters.Add((JsonConverter)new DictionaryConverterFactory()); - //options.Converters.Add((JsonConverter)new CollectionConverterFactory()); - //options.Converters.Add((JsonConverter)new JsonNodeConverterFactory()); - //options.Converters.Add((JsonConverter)new ObjectConverterFactory()); return options; } } diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index a00c834b..0f3b1e2c 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -3,7 +3,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Dahomey.Json; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; @@ -13,7 +12,7 @@ public class SystemTextJsonSerializer: IGraphQLWebsocketJsonSerializer { public static JsonSerializerOptions DefaultJsonSerializerOptions => new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }.SetupDahomeyExtensions(); + }.SetupImmutableConverter(); public JsonSerializerOptions Options { get; } diff --git a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs index 212aefa6..76f9ff78 100644 --- a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs +++ b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Dahomey.Json; using GraphQL.Client.Serializer.SystemTextJson; namespace GraphQL.Client.Serializer.Tests { @@ -9,6 +8,6 @@ public SystemTextJsonSerializerTests() : base(new SystemTextJsonSerializer()) { public class SystemTextJsonSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest { public SystemTextJsonSerializeNoCamelCaseTest() - : base(new SystemTextJsonSerializer(new JsonSerializerOptions().SetupDahomeyExtensions())) { } + : base(new SystemTextJsonSerializer(new JsonSerializerOptions().SetupImmutableConverter())) { } } } diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs index 4f9d694c..404fda64 100644 --- a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs @@ -13,6 +13,10 @@ public IEnumerator GetEnumerator() { "{\"query\":\"simplequerystring\",\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"},\"operationName\":null}", new GraphQLRequest("simple query string", new { camelCaseProperty = "camelCase", PascalCaseProperty = "PascalCase"}) }; + yield return new object[] { + "{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null,\"authentication\":\"an-authentication-token\"}", + new GraphQLRequest("simple query string"){{"authentication", "an-authentication-token"}} + }; } IEnumerator IEnumerable.GetEnumerator() { From 68e6b1f60e5c5b808bd8ba25e49a2754c8d85b89 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 17:27:52 +0100 Subject: [PATCH 24/24] fix GraphQLRequest.Equals --- src/GraphQL.Primitives/GraphQLRequest.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/GraphQL.Primitives/GraphQLRequest.cs b/src/GraphQL.Primitives/GraphQLRequest.cs index ae174392..ed351a10 100644 --- a/src/GraphQL.Primitives/GraphQLRequest.cs +++ b/src/GraphQL.Primitives/GraphQLRequest.cs @@ -64,9 +64,7 @@ public override bool Equals(object? obj) { public virtual bool Equals(GraphQLRequest? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Query == other.Query - && OperationName == other.OperationName - && EqualityComparer.Default.Equals(Variables, other.Variables); + return EqualityComparer>.Default.Equals(this, other); } ///