From d0928cc816ff4edd280644aa3d208219f9879cbb Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Fri, 7 Feb 2020 19:38:32 +0100 Subject: [PATCH 01/55] remove extension fields and use DataMemberName --- src/GraphQL.Primitives/GraphQLError.cs | 10 ---------- src/GraphQL.Primitives/GraphQLRequest.cs | 1 - src/GraphQL.Primitives/GraphQLResponse.cs | 7 +------ 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/GraphQL.Primitives/GraphQLError.cs b/src/GraphQL.Primitives/GraphQLError.cs index c1b159a4..b08105c4 100644 --- a/src/GraphQL.Primitives/GraphQLError.cs +++ b/src/GraphQL.Primitives/GraphQLError.cs @@ -10,12 +10,6 @@ namespace GraphQL { /// public class GraphQLError : IEquatable { - /// - /// The extensions of the error - /// - [DataMember(Name = "extensions")] - public IDictionary? Extensions { get; set; } - /// /// The locations of the error /// @@ -50,7 +44,6 @@ public override bool Equals(object? obj) => public bool Equals(GraphQLError? other) { if (other == null) { return false; } if (ReferenceEquals(this, other)) { return true; } - if (!EqualityComparer?>.Default.Equals(this.Extensions, other.Extensions)) { return false; } { if (this.Locations != null && other.Locations != null) { if (!this.Locations.SequenceEqual(other.Locations)) { return false; } @@ -74,9 +67,6 @@ public bool Equals(GraphQLError? other) { /// public override int GetHashCode() { var hashCode = 0; - if (this.Extensions != null) { - hashCode = hashCode ^ EqualityComparer?>.Default.GetHashCode(this.Extensions); - } if (this.Locations != null) { hashCode = hashCode ^ EqualityComparer.Default.GetHashCode(this.Locations); } diff --git a/src/GraphQL.Primitives/GraphQLRequest.cs b/src/GraphQL.Primitives/GraphQLRequest.cs index 874b0352..13b2a1f8 100644 --- a/src/GraphQL.Primitives/GraphQLRequest.cs +++ b/src/GraphQL.Primitives/GraphQLRequest.cs @@ -12,7 +12,6 @@ public class GraphQLRequest : IEquatable { /// /// The Query /// - /// [DataMember(Name = "query")] public string Query { get; set; } diff --git a/src/GraphQL.Primitives/GraphQLResponse.cs b/src/GraphQL.Primitives/GraphQLResponse.cs index 4ae09061..0145229d 100644 --- a/src/GraphQL.Primitives/GraphQLResponse.cs +++ b/src/GraphQL.Primitives/GraphQLResponse.cs @@ -12,10 +12,7 @@ public class GraphQLResponse : IEquatable?> { [DataMember(Name = "errors")] public GraphQLError[]? Errors { get; set; } - - [DataMember(Name = "extensions")] - public IDictionary? Extensions { get; set; } - + public override bool Equals(object? obj) => this.Equals(obj as GraphQLResponse); public bool Equals(GraphQLResponse? other) { @@ -29,7 +26,6 @@ public bool Equals(GraphQLResponse? other) { else if (this.Errors != null && other.Errors == null) { return false; } else if (this.Errors == null && other.Errors != null) { return false; } } - if (!EqualityComparer?>.Default.Equals(this.Extensions, other.Extensions)) { return false; } return true; } @@ -46,7 +42,6 @@ public override int GetHashCode() { hashCode = (hashCode * 397) ^ 0; } } - hashCode = (hashCode * 397) ^ EqualityComparer?>.Default.GetHashCode(this.Extensions); return hashCode; } } From f019bd6eaaebe249e49b91d99c95ed8eb1b56a4e Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Fri, 7 Feb 2020 20:16:24 +0100 Subject: [PATCH 02/55] create interfaces --- .../Websocket/IGraphQLWebSocketJsonSerializer.cs | 5 +++++ src/GraphQL.Client/GraphQLHttpClient.cs | 5 +++-- src/GraphQL.Client/GraphQLHttpClientOptions.cs | 9 +++------ src/GraphQL.Client/GraphQLHttpResponse.cs | 4 ---- src/GraphQL.Client/IGraphQLJsonSerializer.cs | 12 ++++++++++++ .../Websocket/GraphQLHttpWebsocketHelpers.cs | 9 +++++---- src/GraphQL.Primitives/GraphQLRequest.cs | 6 ++---- 7 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 src/GraphQL.Client.Http/Websocket/IGraphQLWebSocketJsonSerializer.cs create mode 100644 src/GraphQL.Client/IGraphQLJsonSerializer.cs diff --git a/src/GraphQL.Client.Http/Websocket/IGraphQLWebSocketJsonSerializer.cs b/src/GraphQL.Client.Http/Websocket/IGraphQLWebSocketJsonSerializer.cs new file mode 100644 index 00000000..5f89191d --- /dev/null +++ b/src/GraphQL.Client.Http/Websocket/IGraphQLWebSocketJsonSerializer.cs @@ -0,0 +1,5 @@ +namespace GraphQL.Client.Http.Websocket { + public interface IGraphQLWebSocketJsonSerializer: IGraphQLJsonSerializer { + GraphQLWebSocketResponse DeserializeWebSocketResponse(byte[] utf8bytes); + } +} diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 15ff6e8f..16e4b3cf 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -14,6 +14,7 @@ public class GraphQLHttpClient : IGraphQLClient { private readonly GraphQLHttpWebSocket graphQlHttpWebSocket; private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private readonly ConcurrentDictionary, object> subscriptionStreams = new ConcurrentDictionary, object>(); + private IGraphQLJsonSerializer JsonSerializer => Options.JsonSerializer; /// /// the instance of which is used internally @@ -111,12 +112,12 @@ private async Task> SendHttpPostRequestAsync>(Options, cancellationToken); + return await JsonSerializer.DeserializeFromUtf8StreamAsync(bodyStream, cancellationToken); } private HttpRequestMessage GenerateHttpRequestMessage(GraphQLRequest request) { var message = new HttpRequestMessage(HttpMethod.Post, this.Options.EndPoint) { - Content = new StringContent(request.SerializeToJson(Options), Encoding.UTF8, Options.MediaType) + Content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, Options.MediaType) }; if (request is GraphQLHttpRequest httpRequest) diff --git a/src/GraphQL.Client/GraphQLHttpClientOptions.cs b/src/GraphQL.Client/GraphQLHttpClientOptions.cs index dcb0a37e..f3f10df9 100644 --- a/src/GraphQL.Client/GraphQLHttpClientOptions.cs +++ b/src/GraphQL.Client/GraphQLHttpClientOptions.cs @@ -2,8 +2,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; +using GraphQL.Client.Http.Websocket; namespace GraphQL.Client.Http { @@ -18,11 +17,9 @@ public class GraphQLHttpClientOptions { public Uri EndPoint { get; set; } /// - /// The that is going to be used + /// the json serializer /// - public JsonSerializerSettings JsonSerializerSettings { get; set; } = new JsonSerializerSettings { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }; + public IGraphQLWebSocketJsonSerializer JsonSerializer { get; set; } /// /// The that is going to be used diff --git a/src/GraphQL.Client/GraphQLHttpResponse.cs b/src/GraphQL.Client/GraphQLHttpResponse.cs index 1e7c8424..1dbfea30 100644 --- a/src/GraphQL.Client/GraphQLHttpResponse.cs +++ b/src/GraphQL.Client/GraphQLHttpResponse.cs @@ -2,8 +2,4 @@ namespace GraphQL.Client.Http { public class GraphQLHttpResponse : GraphQLResponse { } - - public class GraphQLHttpResponse : GraphQLHttpResponse { - } - } diff --git a/src/GraphQL.Client/IGraphQLJsonSerializer.cs b/src/GraphQL.Client/IGraphQLJsonSerializer.cs new file mode 100644 index 00000000..b9dadd3a --- /dev/null +++ b/src/GraphQL.Client/IGraphQLJsonSerializer.cs @@ -0,0 +1,12 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace GraphQL.Client { + public interface IGraphQLJsonSerializer { + string Serialize(GraphQLRequest request); + + Task> DeserializeFromUtf8StreamAsync(Stream stream, + CancellationToken cancellationToken); + } +} diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index f3cb5854..ff0aa5e1 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -48,7 +48,8 @@ internal static IObservable> CreateSubscriptionStream // post the GraphQLResponse to the stream (even if a GraphQL error occurred) Debug.WriteLine($"received payload on subscription {startRequest.Id}"); var typedResponse = - response.MessageBytes.DeserializeFromBytes>(client.Options); + client.Options.JsonSerializer.DeserializeWebSocketResponse( + response.MessageBytes); o.OnNext(typedResponse.Payload); // in case of a GraphQL error, terminate the sequence after the response has been posted @@ -167,9 +168,9 @@ internal static Task> SendRequest( .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 = - response.MessageBytes.DeserializeFromBytes>(client.Options); + Debug.WriteLine($"received response for request {websocketRequest.Id}"); + var typedResponse = client.Options.JsonSerializer + .DeserializeWebSocketResponse(response.MessageBytes); return typedResponse.Payload; }); diff --git a/src/GraphQL.Primitives/GraphQLRequest.cs b/src/GraphQL.Primitives/GraphQLRequest.cs index 13b2a1f8..d39d2cea 100644 --- a/src/GraphQL.Primitives/GraphQLRequest.cs +++ b/src/GraphQL.Primitives/GraphQLRequest.cs @@ -25,11 +25,9 @@ public class GraphQLRequest : IEquatable { /// Represents the request variables /// [DataMember(Name = "variables")] - public virtual object? Variables { get; set; } + public object? Variables { get; set; } - - public GraphQLRequest() { - } + public GraphQLRequest() { } public GraphQLRequest(string query, object? variables = null, string? operationName = null) { Query = query; From 50827f27cc0e5ef501b410257532660d3f841b9a Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Sun, 9 Feb 2020 22:26:37 +0100 Subject: [PATCH 03/55] move newtonsoft serialization to GraphQL.Client.Serializer.Newtonsoft --- GraphQL.Client.sln | 14 ++++ ...aphQL.Client.Abstractions.Websocket.csproj | 12 +++ .../GraphQLWebSocketMessageType.cs | 2 +- .../GraphQLWebSocketRequest.cs | 2 +- .../GraphQLWebSocketResponse.cs | 10 +-- .../IGraphQLWebsocketJsonSerializer.cs | 13 ++++ .../WebsocketResponseWrapper.cs | 2 +- .../IGraphQLJsonSerializer.cs | 4 +- ...raphQL.Client.Serializer.Newtonsoft.csproj | 16 ++++ .../NewtonsoftJsonSerializer.cs | 48 ++++++++++++ .../NewtonsoftJsonSerializerOptions.cs | 10 +++ src/GraphQL.Client/GraphQL.Client.csproj | 9 +++ src/GraphQL.Client/GraphQLHttpClient.cs | 2 +- .../GraphQLHttpClientOptions.cs | 4 +- .../GraphQLSerializationExtensions.cs | 10 +-- .../Websocket/GraphQLHttpWebSocket.cs | 15 ++-- .../Websocket/GraphQLHttpWebsocketHelpers.cs | 9 ++- src/src.props | 1 + .../ExtensionsTest.cs | 76 +++++++++---------- 19 files changed, 187 insertions(+), 72 deletions(-) create mode 100644 src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj rename src/{GraphQL.Client/Websocket => GraphQL.Client.Abstractions.Websocket}/GraphQLWebSocketMessageType.cs (98%) rename src/{GraphQL.Client/Websocket => GraphQL.Client.Abstractions.Websocket}/GraphQLWebSocketRequest.cs (98%) rename src/{GraphQL.Client/Websocket => GraphQL.Client.Abstractions.Websocket}/GraphQLWebSocketResponse.cs (84%) create mode 100644 src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs rename src/{GraphQL.Client/Websocket => GraphQL.Client.Abstractions.Websocket}/WebsocketResponseWrapper.cs (77%) rename src/{GraphQL.Client => GraphQL.Client.Abstractions}/IGraphQLJsonSerializer.cs (73%) create mode 100644 src/GraphQL.Client.Serializer.Newtonsoft/GraphQL.Client.Serializer.Newtonsoft.csproj create mode 100644 src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs create mode 100644 src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln index 1b6cc421..6d958137 100644 --- a/GraphQL.Client.sln +++ b/GraphQL.Client.sln @@ -58,6 +58,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Abstractions EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client", "src\GraphQL.Client\GraphQL.Client.csproj", "{ED3541C9-D2B2-4D06-A464-38E404A3919A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.Abstractions.Websocket", "src\GraphQL.Client.Abstractions.Websocket\GraphQL.Client.Abstractions.Websocket.csproj", "{4D581CE1-523D-46BF-BAA5-F7D79A1B7654}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Serializer.Newtonsoft", "src\GraphQL.Client.Serializer.Newtonsoft\GraphQL.Client.Serializer.Newtonsoft.csproj", "{11F28E78-ADE4-4153-B97C-56136EB7BD5B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -96,6 +100,14 @@ Global {ED3541C9-D2B2-4D06-A464-38E404A3919A}.Debug|Any CPU.Build.0 = Debug|Any CPU {ED3541C9-D2B2-4D06-A464-38E404A3919A}.Release|Any CPU.ActiveCfg = Release|Any CPU {ED3541C9-D2B2-4D06-A464-38E404A3919A}.Release|Any CPU.Build.0 = Release|Any CPU + {4D581CE1-523D-46BF-BAA5-F7D79A1B7654}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D581CE1-523D-46BF-BAA5-F7D79A1B7654}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D581CE1-523D-46BF-BAA5-F7D79A1B7654}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D581CE1-523D-46BF-BAA5-F7D79A1B7654}.Release|Any CPU.Build.0 = Release|Any CPU + {11F28E78-ADE4-4153-B97C-56136EB7BD5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11F28E78-ADE4-4153-B97C-56136EB7BD5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11F28E78-ADE4-4153-B97C-56136EB7BD5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11F28E78-ADE4-4153-B97C-56136EB7BD5B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -109,6 +121,8 @@ Global {C68C26EB-7659-402A-93D1-E6E248DA5427} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} {76E622F6-7CDD-4B1F-AD06-FFABF37C55E5} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} {ED3541C9-D2B2-4D06-A464-38E404A3919A} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} + {4D581CE1-523D-46BF-BAA5-F7D79A1B7654} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} + {11F28E78-ADE4-4153-B97C-56136EB7BD5B} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4} diff --git a/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj b/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj new file mode 100644 index 00000000..5dc9217a --- /dev/null +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0;netstandard2.1 + 8.0 + + + + + + + diff --git a/src/GraphQL.Client/Websocket/GraphQLWebSocketMessageType.cs b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketMessageType.cs similarity index 98% rename from src/GraphQL.Client/Websocket/GraphQLWebSocketMessageType.cs rename to src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketMessageType.cs index 755a075b..6bf17748 100644 --- a/src/GraphQL.Client/Websocket/GraphQLWebSocketMessageType.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketMessageType.cs @@ -1,4 +1,4 @@ -namespace GraphQL.Client.Http.Websocket { +namespace GraphQL.Client.Abstractions.Websocket { public static class GraphQLWebSocketMessageType { /// diff --git a/src/GraphQL.Client/Websocket/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs similarity index 98% rename from src/GraphQL.Client/Websocket/GraphQLWebSocketRequest.cs rename to src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs index 01dcf173..f43995e2 100644 --- a/src/GraphQL.Client/Websocket/GraphQLWebSocketRequest.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace GraphQL.Client.Http.Websocket { +namespace GraphQL.Client.Abstractions.Websocket { /// /// A Subscription Request diff --git a/src/GraphQL.Client/Websocket/GraphQLWebSocketResponse.cs b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketResponse.cs similarity index 84% rename from src/GraphQL.Client/Websocket/GraphQLWebSocketResponse.cs rename to src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketResponse.cs index 08171a6a..c0fd7bd8 100644 --- a/src/GraphQL.Client/Websocket/GraphQLWebSocketResponse.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketResponse.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace GraphQL.Client.Http.Websocket { +namespace GraphQL.Client.Abstractions.Websocket { /// /// A Subscription Response @@ -60,10 +60,10 @@ public override int GetHashCode() { } - public class GraphQLWebSocketResponse : GraphQLWebSocketResponse, IEquatable> { - public GraphQLHttpResponse Payload { get; set; } + public class GraphQLWebSocketResponse : GraphQLWebSocketResponse, IEquatable> { + public TPayload Payload { get; set; } - public bool Equals(GraphQLWebSocketResponse? other) { + public bool Equals(GraphQLWebSocketResponse? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return base.Equals(other) && Payload.Equals(other.Payload); @@ -73,7 +73,7 @@ public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((GraphQLWebSocketResponse)obj); + return Equals((GraphQLWebSocketResponse)obj); } public override int GetHashCode() { diff --git a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs new file mode 100644 index 00000000..285d69b6 --- /dev/null +++ b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs @@ -0,0 +1,13 @@ +using System; +using System.IO; + +namespace GraphQL.Client.Abstractions.Websocket +{ + public interface IGraphQLWebsocketJsonSerializer: IGraphQLJsonSerializer { + byte[] SerializeToBytes(GraphQLWebSocketRequest request); + + WebsocketResponseWrapper DeserializeToWebsocketResponseWrapper(Stream stream); + GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes); + + } +} diff --git a/src/GraphQL.Client/Websocket/WebsocketResponseWrapper.cs b/src/GraphQL.Client.Abstractions.Websocket/WebsocketResponseWrapper.cs similarity index 77% rename from src/GraphQL.Client/Websocket/WebsocketResponseWrapper.cs rename to src/GraphQL.Client.Abstractions.Websocket/WebsocketResponseWrapper.cs index fd91ef78..5e90a38f 100644 --- a/src/GraphQL.Client/Websocket/WebsocketResponseWrapper.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/WebsocketResponseWrapper.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace GraphQL.Client.Http.Websocket { +namespace GraphQL.Client.Abstractions.Websocket { public class WebsocketResponseWrapper : GraphQLWebSocketResponse { [IgnoreDataMember] diff --git a/src/GraphQL.Client/IGraphQLJsonSerializer.cs b/src/GraphQL.Client.Abstractions/IGraphQLJsonSerializer.cs similarity index 73% rename from src/GraphQL.Client/IGraphQLJsonSerializer.cs rename to src/GraphQL.Client.Abstractions/IGraphQLJsonSerializer.cs index b9dadd3a..f1f6cfb3 100644 --- a/src/GraphQL.Client/IGraphQLJsonSerializer.cs +++ b/src/GraphQL.Client.Abstractions/IGraphQLJsonSerializer.cs @@ -2,9 +2,9 @@ using System.Threading; using System.Threading.Tasks; -namespace GraphQL.Client { +namespace GraphQL.Client.Abstractions { public interface IGraphQLJsonSerializer { - string Serialize(GraphQLRequest request); + string SerializeToString(GraphQLRequest request); Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken); diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQL.Client.Serializer.Newtonsoft.csproj b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQL.Client.Serializer.Newtonsoft.csproj new file mode 100644 index 00000000..9ae19080 --- /dev/null +++ b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQL.Client.Serializer.Newtonsoft.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0;netstandard2.1 + 8.0 + + + + + + + + + + + diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs new file mode 100644 index 00000000..00b59fb7 --- /dev/null +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions.Websocket; +using Newtonsoft.Json; + +namespace GraphQL.Client.Serializer.Newtonsoft +{ + public class NewtonsoftJsonSerializer: IGraphQLWebsocketJsonSerializer + { + public NewtonsoftJsonSerializerOptions Options { get; } + + public NewtonsoftJsonSerializer(NewtonsoftJsonSerializerOptions options) { + Options = options; + } + + public string SerializeToString(GraphQLRequest request) { + return JsonConvert.SerializeObject(request, Options.JsonSerializerSettings); + } + + public byte[] SerializeToBytes(GraphQLWebSocketRequest request) { + var json = JsonConvert.SerializeObject(request, Options.JsonSerializerSettings); + return Encoding.UTF8.GetBytes(json); + } + + public WebsocketResponseWrapper DeserializeToWebsocketResponseWrapper(Stream stream) { + using (StreamReader sr = new StreamReader(stream)) + using (JsonReader reader = new JsonTextReader(sr)) { + JsonSerializer serializer = JsonSerializer.Create(Options.JsonSerializerSettings); + + var wrapper = serializer.Deserialize(reader); + return wrapper; + } + } + + public GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes) { + return JsonConvert.DeserializeObject>>(Encoding.UTF8.GetString(bytes), + Options.JsonSerializerSettings); + } + + public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { + throw new NotImplementedException(); + } + + } +} diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs new file mode 100644 index 00000000..9148ed2e --- /dev/null +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace GraphQL.Client.Serializer.Newtonsoft { + public class NewtonsoftJsonSerializerOptions { + public JsonSerializerSettings JsonSerializerSettings { get; set; } = new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + } +} diff --git a/src/GraphQL.Client/GraphQL.Client.csproj b/src/GraphQL.Client/GraphQL.Client.csproj index 2dfd5ad8..640f9b23 100644 --- a/src/GraphQL.Client/GraphQL.Client.csproj +++ b/src/GraphQL.Client/GraphQL.Client.csproj @@ -15,6 +15,14 @@ NETFRAMEWORK + + + + + + + + @@ -25,6 +33,7 @@ + diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 16e4b3cf..361b6195 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -117,7 +117,7 @@ private async Task> SendHttpPostRequestAsync /// the json serializer /// - public IGraphQLWebSocketJsonSerializer JsonSerializer { get; set; } + public IGraphQLWebsocketJsonSerializer JsonSerializer { get; set; } /// /// The that is going to be used diff --git a/src/GraphQL.Client/GraphQLSerializationExtensions.cs b/src/GraphQL.Client/GraphQLSerializationExtensions.cs index 649d2c73..8d702204 100644 --- a/src/GraphQL.Client/GraphQLSerializationExtensions.cs +++ b/src/GraphQL.Client/GraphQLSerializationExtensions.cs @@ -2,6 +2,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http.Websocket; using Newtonsoft.Json; @@ -18,20 +19,15 @@ public static byte[] SerializeToBytes(this GraphQLRequest request, var json = JsonConvert.SerializeObject(request, options.JsonSerializerSettings); return Encoding.UTF8.GetBytes(json); } - public static byte[] SerializeToBytes(this GraphQLWebSocketRequest request, - GraphQLHttpClientOptions options) { - var json = JsonConvert.SerializeObject(request, options.JsonSerializerSettings); - return Encoding.UTF8.GetBytes(json); - } public static TGraphQLResponse DeserializeFromJson(this string jsonString, GraphQLHttpClientOptions options) { return JsonConvert.DeserializeObject(jsonString, options.JsonSerializerSettings); } - public static TObject DeserializeFromBytes(this byte[] utf8bytes, + public static TObject DeserializeFromBytes(this byte[] utf8Bytes, GraphQLHttpClientOptions options) { - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(utf8bytes), options.JsonSerializerSettings); + return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(utf8Bytes), options.JsonSerializerSettings); } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 5a4e76c2..d5bdcc04 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -9,7 +9,7 @@ using System.Reactive.Threading.Tasks; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; +using GraphQL.Client.Abstractions.Websocket; namespace GraphQL.Client.Http.Websocket { internal class GraphQLHttpWebSocket : IDisposable { @@ -59,7 +59,7 @@ private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request) { } await InitializeWebSocket().ConfigureAwait(false); - var requestBytes = request.SerializeToBytes(_options); + var requestBytes = _options.JsonSerializer.SerializeToBytes(request); await this.clientWebSocket.SendAsync( new ArraySegment(requestBytes), WebSocketMessageType.Text, @@ -223,14 +223,9 @@ private async Task _receiveResultAsync() { ms.Seek(0, SeekOrigin.Begin); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { - using (StreamReader sr = new StreamReader(ms)) - using (JsonReader reader = new JsonTextReader(sr)) { - JsonSerializer serializer = JsonSerializer.Create(_options.JsonSerializerSettings); - - var response = serializer.Deserialize(reader); - response.MessageBytes = ms.ToArray(); - return response; - } + var response = _options.JsonSerializer.DeserializeToWebsocketResponseWrapper(ms); + response.MessageBytes = ms.ToArray(); + return response; } else { throw new NotSupportedException("binary websocket messages are not supported"); diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index ff0aa5e1..9148a727 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -6,7 +6,7 @@ using System.Reactive.Threading.Tasks; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; +using GraphQL.Client.Abstractions.Websocket; namespace GraphQL.Client.Http.Websocket { public static class GraphQLHttpWebsocketHelpers { @@ -48,7 +48,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}"); var typedResponse = - client.Options.JsonSerializer.DeserializeWebSocketResponse( + client.Options.JsonSerializer.DeserializeToWebsocketResponse( response.MessageBytes); o.OnNext(typedResponse.Payload); @@ -169,8 +169,9 @@ internal static Task> SendRequest( .TakeUntil(response => response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) .Select(response => { Debug.WriteLine($"received response for request {websocketRequest.Id}"); - var typedResponse = client.Options.JsonSerializer - .DeserializeWebSocketResponse(response.MessageBytes); + var typedResponse = + client.Options.JsonSerializer.DeserializeToWebsocketResponse( + response.MessageBytes); return typedResponse.Payload; }); diff --git a/src/src.props b/src/src.props index 19e1fafb..e2e2512b 100644 --- a/src/src.props +++ b/src/src.props @@ -5,6 +5,7 @@ true + 8.0 diff --git a/tests/GraphQL.Integration.Tests/ExtensionsTest.cs b/tests/GraphQL.Integration.Tests/ExtensionsTest.cs index fff1b6e4..1ffaabd2 100644 --- a/tests/GraphQL.Integration.Tests/ExtensionsTest.cs +++ b/tests/GraphQL.Integration.Tests/ExtensionsTest.cs @@ -13,48 +13,48 @@ public class ExtensionsTest { private static TestServerSetup SetupTest(bool requestsViaWebsocket = false) => WebHostHelpers.SetupTest(requestsViaWebsocket); - [Fact] - public async void CanDeserializeExtensions() { + //[Fact] + //public async void CanDeserializeExtensions() { - using var setup = SetupTest(); - var response = await setup.Client.SendQueryAsync(new GraphQLRequest("query { extensionsTest }"), - () => new {extensionsTest = ""}) - .ConfigureAwait(false); + // using var setup = SetupTest(); + // var response = await setup.Client.SendQueryAsync(new GraphQLRequest("query { extensionsTest }"), + // () => new {extensionsTest = ""}) + // .ConfigureAwait(false); - response.Errors.Should().NotBeNull(); - response.Errors.Should().ContainSingle(); - response.Errors[0].Extensions.Should().NotBeNull(); - response.Errors[0].Extensions.Should().ContainKey("data"); + // response.Errors.Should().NotBeNull(); + // response.Errors.Should().ContainSingle(); + // response.Errors[0].Extensions.Should().NotBeNull(); + // response.Errors[0].Extensions.Should().ContainKey("data"); - foreach (var item in ChatQuery.TestExtensions) { + // foreach (var item in ChatQuery.TestExtensions) { - } - } - - [Fact] - public async void DontNeedToUseCamelCaseNamingStrategy() { - - using var setup = SetupTest(); - setup.Client.Options.JsonSerializerSettings = new JsonSerializerSettings(); - - const string message = "some random testing message"; - var graphQLRequest = new GraphQLRequest( - @"mutation($input: MessageInputType){ - addMessage(message: $input){ - content - } - }", - new { - input = new { - fromId = "2", - content = message, - sentAt = DateTime.Now - } - }); - var response = await setup.Client.SendMutationAsync(graphQLRequest, () => new { addMessage = new { content = "" } }); - - Assert.Equal(message, response.Data.addMessage.content); - } + // } + //} + + //[Fact] + //public async void DontNeedToUseCamelCaseNamingStrategy() { + + // using var setup = SetupTest(); + // setup.Client.Options.JsonSerializerSettings = new JsonSerializerSettings(); + + // const string message = "some random testing message"; + // var graphQLRequest = new GraphQLRequest( + // @"mutation($input: MessageInputType){ + // addMessage(message: $input){ + // content + // } + // }", + // new { + // input = new { + // fromId = "2", + // content = message, + // sentAt = DateTime.Now + // } + // }); + // var response = await setup.Client.SendMutationAsync(graphQLRequest, () => new { addMessage = new { content = "" } }); + + // Assert.Equal(message, response.Data.addMessage.content); + //} } } From 5ece68c821d0b2ff5537a71f92027d432db0304d Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Sun, 9 Feb 2020 22:29:27 +0100 Subject: [PATCH 04/55] add constructor for di --- src/GraphQL.Client/GraphQLHttpClient.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 361b6195..2d7794d2 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using GraphQL.Client.Abstractions; +using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http.Websocket; namespace GraphQL.Client.Http { @@ -52,6 +53,13 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); } + public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient, IGraphQLWebsocketJsonSerializer serializer) { + Options = options; + Options.JsonSerializer = serializer; + this.HttpClient = httpClient; + this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); + } + /// public Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { return Options.UseWebSocketForQueriesAndMutations From c4b5e0fee6af4e9eb83da18a49024200e0654273 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 11 Feb 2020 14:44:45 +0100 Subject: [PATCH 05/55] create Extension classes to allow for custom json converters --- src/GraphQL.Primitives/GraphQLError.cs | 10 ++++++++++ src/GraphQL.Primitives/GraphQLResponse.cs | 9 ++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/GraphQL.Primitives/GraphQLError.cs b/src/GraphQL.Primitives/GraphQLError.cs index b08105c4..7c0b0c62 100644 --- a/src/GraphQL.Primitives/GraphQLError.cs +++ b/src/GraphQL.Primitives/GraphQLError.cs @@ -28,6 +28,12 @@ public class GraphQLError : IEquatable { [DataMember(Name = "path")] public object[]? Path { get; set; } + /// + /// The extensions of the error + /// + [DataMember(Name = "extensions")] + public ExtensionsType? Extensions { get; set; } + /// /// Returns a value that indicates whether this instance is equal to a specified object /// @@ -95,6 +101,10 @@ public override int GetHashCode() { public static bool operator !=(GraphQLError? left, GraphQLError? right) => !EqualityComparer.Default.Equals(left, right); + /// + /// The GraphQL extensions type. Create a custom json converter for this class to customize your serializers behaviour + /// + public class ExtensionsType : Dictionary { } } } diff --git a/src/GraphQL.Primitives/GraphQLResponse.cs b/src/GraphQL.Primitives/GraphQLResponse.cs index 0145229d..aa58ff77 100644 --- a/src/GraphQL.Primitives/GraphQLResponse.cs +++ b/src/GraphQL.Primitives/GraphQLResponse.cs @@ -12,7 +12,10 @@ public class GraphQLResponse : IEquatable?> { [DataMember(Name = "errors")] public GraphQLError[]? Errors { get; set; } - + + [DataMember(Name = "extensions")] + public ExtensionsType? Extensions { get; set; } + public override bool Equals(object? obj) => this.Equals(obj as GraphQLResponse); public bool Equals(GraphQLResponse? other) { @@ -51,6 +54,10 @@ public override int GetHashCode() { public static bool operator !=(GraphQLResponse? response1, GraphQLResponse? response2) => !(response1 == response2); + /// + /// The GraphQL extensions type. Create a custom json converter for this class to customize your serializers behaviour + /// + public class ExtensionsType : Dictionary { } } From 0609ea3ba534553ad31f5c4da1a4af93170b41d8 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 11 Feb 2020 15:07:44 +0100 Subject: [PATCH 06/55] automatically create the first discovered serializer if none has been provided --- .../IGraphQLWebsocketJsonSerializer.cs | 4 +++ .../NewtonsoftJsonSerializer.cs | 27 ++++++++++++------- src/GraphQL.Client/GraphQL.Client.csproj | 1 + src/GraphQL.Client/GraphQLHttpClient.cs | 24 +++++++++++++++-- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs index 285d69b6..10448b80 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs @@ -3,6 +3,10 @@ namespace GraphQL.Client.Abstractions.Websocket { + /// + /// The json serializer interface for the graphql-dotnet http client. + /// Implementations should provide a parameterless constructor for convenient usage + /// public interface IGraphQLWebsocketJsonSerializer: IGraphQLJsonSerializer { byte[] SerializeToBytes(GraphQLWebSocketRequest request); diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index 00b59fb7..08afd595 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -12,6 +12,11 @@ public class NewtonsoftJsonSerializer: IGraphQLWebsocketJsonSerializer { public NewtonsoftJsonSerializerOptions Options { get; } + public NewtonsoftJsonSerializer() + { + Options = new NewtonsoftJsonSerializerOptions(); + } + public NewtonsoftJsonSerializer(NewtonsoftJsonSerializerOptions options) { Options = options; } @@ -26,13 +31,7 @@ public byte[] SerializeToBytes(GraphQLWebSocketRequest request) { } public WebsocketResponseWrapper DeserializeToWebsocketResponseWrapper(Stream stream) { - using (StreamReader sr = new StreamReader(stream)) - using (JsonReader reader = new JsonTextReader(sr)) { - JsonSerializer serializer = JsonSerializer.Create(Options.JsonSerializerSettings); - - var wrapper = serializer.Deserialize(reader); - return wrapper; - } + return DeserializeFromUtf8Stream(stream); } public GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes) { @@ -41,8 +40,16 @@ public GraphQLWebSocketResponse> DeserializeToWebsock } public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { - throw new NotImplementedException(); - } + return Task.FromResult(DeserializeFromUtf8Stream>(stream)); + } + + + private T DeserializeFromUtf8Stream(Stream stream) { + using StreamReader sr = new StreamReader(stream); + using JsonReader reader = new JsonTextReader(sr); + JsonSerializer serializer = JsonSerializer.Create(Options.JsonSerializerSettings); + return serializer.Deserialize(reader); + } - } + } } diff --git a/src/GraphQL.Client/GraphQL.Client.csproj b/src/GraphQL.Client/GraphQL.Client.csproj index 640f9b23..31fa21ab 100644 --- a/src/GraphQL.Client/GraphQL.Client.csproj +++ b/src/GraphQL.Client/GraphQL.Client.csproj @@ -35,6 +35,7 @@ + diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 2d7794d2..3f5bd89d 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.Linq; using System.Net.Http; using System.Text; using System.Threading; @@ -39,24 +40,27 @@ public GraphQLHttpClient(Action configure) { configure(Options); this.HttpClient = new HttpClient(Options.HttpMessageHandler); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); + EnsureSerializerAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options) { Options = options; this.HttpClient = new HttpClient(Options.HttpMessageHandler); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); + EnsureSerializerAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient) { Options = options; this.HttpClient = httpClient; this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); + EnsureSerializerAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient, IGraphQLWebsocketJsonSerializer serializer) { Options = options; - Options.JsonSerializer = serializer; - this.HttpClient = httpClient; + Options.JsonSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + this.HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); } @@ -111,6 +115,22 @@ public IObservable> CreateSubscriptionStream s.GetTypes()) + .FirstOrDefault(p => type.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract); + if(serializerType == null) + throw new InvalidOperationException($"no implementation of \"{type}\" found"); + + Options.JsonSerializer = (IGraphQLWebsocketJsonSerializer) Activator.CreateInstance(serializerType); + } + private async Task> SendHttpPostRequestAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { var preprocessedRequest = await Options.PreprocessRequest(request, this); using var httpRequestMessage = this.GenerateHttpRequestMessage(preprocessedRequest); From b138e27940f876c3bc1f39ecba817452679b1320 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 11 Feb 2020 16:13:37 +0100 Subject: [PATCH 07/55] create LocalExecutionClient --- GraphQL.Client.sln | 9 +- ...aphQL.Client.Abstractions.Websocket.csproj | 13 ++- .../IGraphQLWebsocketJsonSerializer.cs | 1 - .../GraphQLJsonSerializerExtensions.cs | 22 ++++ .../IGraphQLClient.cs | 5 - .../IGraphQLJsonSerializer.cs | 2 + .../ExecutionResultExtensions.cs | 5 + .../GraphQL.Client.LocalExecution.csproj | 20 ++++ .../GraphQLEnumConverter.cs | 31 +++++ .../GraphQLLocalExecutionClient.cs | 109 ++++++++++++++++++ src/GraphQL.Client/GraphQLHttpClient.cs | 24 +--- 11 files changed, 208 insertions(+), 33 deletions(-) create mode 100644 src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs create mode 100644 src/GraphQL.Client.LocalExecution/ExecutionResultExtensions.cs create mode 100644 src/GraphQL.Client.LocalExecution/GraphQL.Client.LocalExecution.csproj create mode 100644 src/GraphQL.Client.LocalExecution/GraphQLEnumConverter.cs create mode 100644 src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln index 6d958137..071e73f1 100644 --- a/GraphQL.Client.sln +++ b/GraphQL.Client.sln @@ -58,10 +58,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Abstractions EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client", "src\GraphQL.Client\GraphQL.Client.csproj", "{ED3541C9-D2B2-4D06-A464-38E404A3919A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.Abstractions.Websocket", "src\GraphQL.Client.Abstractions.Websocket\GraphQL.Client.Abstractions.Websocket.csproj", "{4D581CE1-523D-46BF-BAA5-F7D79A1B7654}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Abstractions.Websocket", "src\GraphQL.Client.Abstractions.Websocket\GraphQL.Client.Abstractions.Websocket.csproj", "{4D581CE1-523D-46BF-BAA5-F7D79A1B7654}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Serializer.Newtonsoft", "src\GraphQL.Client.Serializer.Newtonsoft\GraphQL.Client.Serializer.Newtonsoft.csproj", "{11F28E78-ADE4-4153-B97C-56136EB7BD5B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.LocalExecution", "src\GraphQL.Client.LocalExecution\GraphQL.Client.LocalExecution.csproj", "{2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -108,6 +110,10 @@ Global {11F28E78-ADE4-4153-B97C-56136EB7BD5B}.Debug|Any CPU.Build.0 = Debug|Any CPU {11F28E78-ADE4-4153-B97C-56136EB7BD5B}.Release|Any CPU.ActiveCfg = Release|Any CPU {11F28E78-ADE4-4153-B97C-56136EB7BD5B}.Release|Any CPU.Build.0 = Release|Any CPU + {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -123,6 +129,7 @@ Global {ED3541C9-D2B2-4D06-A464-38E404A3919A} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} {4D581CE1-523D-46BF-BAA5-F7D79A1B7654} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} {11F28E78-ADE4-4153-B97C-56136EB7BD5B} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} + {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4} diff --git a/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj b/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj index 5dc9217a..30202e85 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj @@ -1,12 +1,13 @@ + - - netstandard2.0;netstandard2.1 + + netstandard2.0;netstandard2.1 8.0 - + - - - + + + diff --git a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs index 10448b80..7a215911 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs @@ -1,4 +1,3 @@ -using System; using System.IO; namespace GraphQL.Client.Abstractions.Websocket diff --git a/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs b/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs new file mode 100644 index 00000000..8e574e57 --- /dev/null +++ b/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; + +namespace GraphQL.Client.Abstractions { + public static class GraphQLJsonSerializerExtensions { + public static void EnsureAssigned(this TSerializerInterface jsonSerializer) where TSerializerInterface: IGraphQLJsonSerializer { + // return if a serializer was assigned + if (jsonSerializer != null) return; + + // else try to find one in the assembly and assign that + var type = typeof(TSerializerInterface); + var serializerType = AppDomain.CurrentDomain + .GetAssemblies() + .SelectMany(s => s.GetTypes()) + .FirstOrDefault(p => type.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract); + if (serializerType == null) + throw new InvalidOperationException($"no implementation of \"{type}\" found"); + + jsonSerializer = (TSerializerInterface)Activator.CreateInstance(serializerType); + } + } +} diff --git a/src/GraphQL.Client.Abstractions/IGraphQLClient.cs b/src/GraphQL.Client.Abstractions/IGraphQLClient.cs index 3b47a932..6889a179 100644 --- a/src/GraphQL.Client.Abstractions/IGraphQLClient.cs +++ b/src/GraphQL.Client.Abstractions/IGraphQLClient.cs @@ -31,11 +31,6 @@ public interface IGraphQLClient : IDisposable { /// an external handler for all s occuring within the sequence /// an observable stream for the specified subscription IObservable> CreateSubscriptionStream(GraphQLRequest request, Action exceptionHandler); - - /// - /// Publishes all exceptions which occur inside the websocket receive stream (i.e. for logging purposes) - /// - IObservable WebSocketReceiveErrors { get; } } } diff --git a/src/GraphQL.Client.Abstractions/IGraphQLJsonSerializer.cs b/src/GraphQL.Client.Abstractions/IGraphQLJsonSerializer.cs index f1f6cfb3..d29378ca 100644 --- a/src/GraphQL.Client.Abstractions/IGraphQLJsonSerializer.cs +++ b/src/GraphQL.Client.Abstractions/IGraphQLJsonSerializer.cs @@ -9,4 +9,6 @@ public interface IGraphQLJsonSerializer { Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken); } + + } diff --git a/src/GraphQL.Client.LocalExecution/ExecutionResultExtensions.cs b/src/GraphQL.Client.LocalExecution/ExecutionResultExtensions.cs new file mode 100644 index 00000000..a8ce44d8 --- /dev/null +++ b/src/GraphQL.Client.LocalExecution/ExecutionResultExtensions.cs @@ -0,0 +1,5 @@ +namespace GraphQL.Client.LocalExecution { + public class ExecutionResultExtensions { + + } +} diff --git a/src/GraphQL.Client.LocalExecution/GraphQL.Client.LocalExecution.csproj b/src/GraphQL.Client.LocalExecution/GraphQL.Client.LocalExecution.csproj new file mode 100644 index 00000000..3cd19e04 --- /dev/null +++ b/src/GraphQL.Client.LocalExecution/GraphQL.Client.LocalExecution.csproj @@ -0,0 +1,20 @@ + + + + + + A GraphQL Client which executes the queries directly on a provided C# schema + netstandard2.0 + + + + + + + + + + + + + diff --git a/src/GraphQL.Client.LocalExecution/GraphQLEnumConverter.cs b/src/GraphQL.Client.LocalExecution/GraphQLEnumConverter.cs new file mode 100644 index 00000000..a9919454 --- /dev/null +++ b/src/GraphQL.Client.LocalExecution/GraphQLEnumConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Reflection; +using GraphQL.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace GraphQL.Client.LocalExecution { + public class GraphQLEnumConverter : StringEnumConverter { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { + if (value == null) { + writer.WriteNull(); + } + else { + var enumString = ((Enum)value).ToString("G"); + var memberName = value.GetType() + .GetMember(enumString, BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public) + .FirstOrDefault()?.Name; + if (string.IsNullOrEmpty(memberName)) { + if (!AllowIntegerValues) + throw new JsonSerializationException($"Integer value {value} is not allowed."); + writer.WriteValue(value); + } + else { + writer.WriteValue(StringUtils.ToConstantCase(memberName)); + } + } + } + } + +} diff --git a/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs new file mode 100644 index 00000000..763be879 --- /dev/null +++ b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions; +using GraphQL.Subscription; +using GraphQL.Types; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace GraphQL.Client.LocalExecution +{ + public class GraphQLLocalExecutionClient: IGraphQLClient where TSchema: ISchema + { + + private static readonly JsonSerializerSettings VariablesSerializerSettings = new JsonSerializerSettings { + Formatting = Formatting.Indented, + DateTimeZoneHandling = DateTimeZoneHandling.Local, + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Converters = new List + { + new GraphQLEnumConverter() + } + }; + + public TSchema Schema { get; } + public IGraphQLJsonSerializer Serializer { get; } + + + private readonly DocumentExecuter documentExecuter; + + public GraphQLLocalExecutionClient(TSchema schema) { + Serializer.EnsureAssigned(); + Schema = schema; + if (!Schema.Initialized) Schema.Initialize(); + documentExecuter = new DocumentExecuter(); + } + + public GraphQLLocalExecutionClient(TSchema schema, IGraphQLJsonSerializer serializer) : this(schema) { + Serializer = serializer; + } + + public void Dispose() { } + + public Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) + => ExecuteQueryAsync(request, cancellationToken); + + public Task> SendMutationAsync(GraphQLRequest request, CancellationToken cancellationToken = default) + => ExecuteQueryAsync(request, cancellationToken); + + public IObservable> CreateSubscriptionStream(GraphQLRequest request) { + return Observable.Defer(() => ExecuteSubscriptionAsync(request).ToObservable()) + .Concat() + .Publish() + .RefCount(); + } + + public IObservable> CreateSubscriptionStream(GraphQLRequest request, + Action exceptionHandler) + => CreateSubscriptionStream(request); + + #region Private Methods + + private async Task> ExecuteQueryAsync(GraphQLRequest request, CancellationToken cancellationToken) { + var executionResult = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false); + return await ExecutionResultToGraphQLResponse(executionResult, cancellationToken).ConfigureAwait(false); + } + private async Task>> ExecuteSubscriptionAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { + var result = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false); + return ((SubscriptionExecutionResult)result).Streams?.Values.SingleOrDefault()? + .SelectMany(executionResult => Observable.FromAsync(token => ExecutionResultToGraphQLResponse(executionResult, token))); + } + + private async Task ExecuteAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { + var serializedRequest = Serializer.SerializeToString(request); + + var deserializedRequest = JsonConvert.DeserializeObject(serializedRequest); + var inputs = deserializedRequest.Variables != null + ? (JObject.FromObject(request.Variables, JsonSerializer.Create(VariablesSerializerSettings)) as JObject) + .ToInputs() + : null; + + var result = await documentExecuter.ExecuteAsync(options => { + options.Schema = Schema; + options.OperationName = request.OperationName; + options.Query = request.Query; + options.Inputs = inputs; + options.CancellationToken = cancellationToken; + }).ConfigureAwait(false); + + return result; + } + + private Task> ExecutionResultToGraphQLResponse(ExecutionResult executionResult, CancellationToken cancellationToken = default) { + // serialize result into utf8 byte stream + var resultStream = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(executionResult, VariablesSerializerSettings))); + // deserialize using the provided serializer + return Serializer.DeserializeFromUtf8StreamAsync(resultStream, cancellationToken); + } + + #endregion + } +} diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 3f5bd89d..b59eb820 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -16,7 +16,7 @@ public class GraphQLHttpClient : IGraphQLClient { private readonly GraphQLHttpWebSocket graphQlHttpWebSocket; private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private readonly ConcurrentDictionary, object> subscriptionStreams = new ConcurrentDictionary, object>(); - private IGraphQLJsonSerializer JsonSerializer => Options.JsonSerializer; + private IGraphQLWebsocketJsonSerializer JsonSerializer => Options.JsonSerializer; /// /// the instance of which is used internally @@ -40,21 +40,21 @@ public GraphQLHttpClient(Action configure) { configure(Options); this.HttpClient = new HttpClient(Options.HttpMessageHandler); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); - EnsureSerializerAssigned(); + JsonSerializer.EnsureAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options) { Options = options; this.HttpClient = new HttpClient(Options.HttpMessageHandler); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); - EnsureSerializerAssigned(); + JsonSerializer.EnsureAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient) { Options = options; this.HttpClient = httpClient; this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); - EnsureSerializerAssigned(); + JsonSerializer.EnsureAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient, IGraphQLWebsocketJsonSerializer serializer) { @@ -115,22 +115,6 @@ public IObservable> CreateSubscriptionStream s.GetTypes()) - .FirstOrDefault(p => type.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract); - if(serializerType == null) - throw new InvalidOperationException($"no implementation of \"{type}\" found"); - - Options.JsonSerializer = (IGraphQLWebsocketJsonSerializer) Activator.CreateInstance(serializerType); - } - private async Task> SendHttpPostRequestAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { var preprocessedRequest = await Options.PreprocessRequest(request, this); using var httpRequestMessage = this.GenerateHttpRequestMessage(preprocessedRequest); From a2dc54074d11c610265c65fc4e237bd8c9db5aa2 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 11 Feb 2020 18:24:04 +0100 Subject: [PATCH 08/55] create test setup for serializers --- GraphQL.Client.sln | 14 ++++ .../GraphQL.Client.Abstractions.csproj | 2 +- .../GraphQLJsonSerializerExtensions.cs | 26 +++--- .../GraphQLLocalExecutionClient.cs | 12 ++- .../NewtonsoftJsonSerializer.cs | 11 ++- src/GraphQL.Client/GraphQLHttpClient.cs | 6 +- .../BaseSerializerTest.cs | 79 +++++++++++++++++++ .../GraphQL.Client.Serializer.Tests.csproj | 24 ++++++ .../NewtonsoftSerializerTest.cs | 30 +++++++ .../Chat/AddMessageMutationResult.cs | 9 +++ .../Chat/AddMessageVariables.cs | 15 ++++ .../Chat/GraphQLClientChatExtensions.cs | 39 +++++++++ .../Chat/JoinDeveloperMutationResult.cs | 10 +++ .../Schema}/CapitalizedFieldsGraphType.cs | 7 +- .../Chat/Schema}/ChatMutation.cs | 2 +- .../Chat/Schema}/ChatQuery.cs | 3 +- .../Chat/Schema/ChatSchema.cs | 10 +++ .../Chat/Schema}/ChatSubscriptions.cs | 3 +- .../Chat/Schema}/IChat.cs | 2 +- .../Chat/Schema}/Message.cs | 2 +- .../Chat/Schema}/MessageFrom.cs | 2 +- .../Chat/Schema}/MessageFromType.cs | 2 +- .../Chat/Schema}/MessageType.cs | 2 +- .../Chat/Schema}/ReceivedMessage.cs | 2 +- tests/GraphQL.Client.Tests.Common/Common.cs | 46 +++++++++++ .../GraphQL.Client.Tests.Common.csproj | 20 +++++ .../StarWars}/StarWarsHumans.cs | 2 +- .../Extensions/GraphQLClientTestExtensions.cs | 15 ++-- .../ExtensionsTest.cs | 26 ------ .../QueryAndMutationTests.cs | 2 +- .../ChatSchema/ChatSchema.cs | 11 --- .../IntegrationTestServer.csproj | 8 +- tests/IntegrationTestServer/StartupChat.cs | 15 ++-- .../IntegrationTestServer/StartupStarWars.cs | 13 +-- 34 files changed, 365 insertions(+), 107 deletions(-) create mode 100644 tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs create mode 100644 tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj create mode 100644 tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs create mode 100644 tests/GraphQL.Client.Tests.Common/Chat/AddMessageMutationResult.cs create mode 100644 tests/GraphQL.Client.Tests.Common/Chat/AddMessageVariables.cs create mode 100644 tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs create mode 100644 tests/GraphQL.Client.Tests.Common/Chat/JoinDeveloperMutationResult.cs rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/CapitalizedFieldsGraphType.cs (58%) rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/ChatMutation.cs (94%) rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/ChatQuery.cs (91%) create mode 100644 tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatSchema.cs rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/ChatSubscriptions.cs (97%) rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/IChat.cs (97%) rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/Message.cs (79%) rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/MessageFrom.cs (67%) rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/MessageFromType.cs (76%) rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/MessageType.cs (88%) rename tests/{IntegrationTestServer/ChatSchema => GraphQL.Client.Tests.Common/Chat/Schema}/ReceivedMessage.cs (76%) create mode 100644 tests/GraphQL.Client.Tests.Common/Common.cs create mode 100644 tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj rename tests/{GraphQL.Integration.Tests/TestData => GraphQL.Client.Tests.Common/StarWars}/StarWarsHumans.cs (87%) delete mode 100644 tests/IntegrationTestServer/ChatSchema/ChatSchema.cs diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln index 071e73f1..df866b4e 100644 --- a/GraphQL.Client.sln +++ b/GraphQL.Client.sln @@ -64,6 +64,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Serializer.N EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.LocalExecution", "src\GraphQL.Client.LocalExecution\GraphQL.Client.LocalExecution.csproj", "{2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.Serializer.Tests", "tests\GraphQL.Client.Serializer.Tests\GraphQL.Client.Serializer.Tests.csproj", "{CA842D18-FC4A-4281-B1FF-080FA91887B8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.Tests.Common", "tests\GraphQL.Client.Tests.Common\GraphQL.Client.Tests.Common.csproj", "{0D307BAD-27AE-4A5D-8764-4AA2620B01E9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,6 +118,14 @@ Global {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}.Release|Any CPU.Build.0 = Release|Any CPU + {CA842D18-FC4A-4281-B1FF-080FA91887B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA842D18-FC4A-4281-B1FF-080FA91887B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA842D18-FC4A-4281-B1FF-080FA91887B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA842D18-FC4A-4281-B1FF-080FA91887B8}.Release|Any CPU.Build.0 = Release|Any CPU + {0D307BAD-27AE-4A5D-8764-4AA2620B01E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D307BAD-27AE-4A5D-8764-4AA2620B01E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D307BAD-27AE-4A5D-8764-4AA2620B01E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D307BAD-27AE-4A5D-8764-4AA2620B01E9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -130,6 +142,8 @@ Global {4D581CE1-523D-46BF-BAA5-F7D79A1B7654} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} {11F28E78-ADE4-4153-B97C-56136EB7BD5B} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} + {CA842D18-FC4A-4281-B1FF-080FA91887B8} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} + {0D307BAD-27AE-4A5D-8764-4AA2620B01E9} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4} diff --git a/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj b/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj index 6430f4b7..78972572 100644 --- a/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj +++ b/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs b/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs index 8e574e57..b652c782 100644 --- a/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs +++ b/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs @@ -3,20 +3,22 @@ namespace GraphQL.Client.Abstractions { public static class GraphQLJsonSerializerExtensions { - public static void EnsureAssigned(this TSerializerInterface jsonSerializer) where TSerializerInterface: IGraphQLJsonSerializer { - // return if a serializer was assigned - if (jsonSerializer != null) return; + public static TSerializerInterface EnsureAssigned(this TSerializerInterface jsonSerializer) where TSerializerInterface: IGraphQLJsonSerializer { + // if no serializer was assigned + if (jsonSerializer == null) { + // try to find one in the assembly and assign that + var type = typeof(TSerializerInterface); + var serializerType = AppDomain.CurrentDomain + .GetAssemblies() + .SelectMany(s => s.GetTypes()) + .FirstOrDefault(p => type.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract); + if (serializerType == null) + throw new InvalidOperationException($"no implementation of \"{type}\" found"); - // else try to find one in the assembly and assign that - var type = typeof(TSerializerInterface); - var serializerType = AppDomain.CurrentDomain - .GetAssemblies() - .SelectMany(s => s.GetTypes()) - .FirstOrDefault(p => type.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract); - if (serializerType == null) - throw new InvalidOperationException($"no implementation of \"{type}\" found"); + jsonSerializer = (TSerializerInterface)Activator.CreateInstance(serializerType); + } - jsonSerializer = (TSerializerInterface)Activator.CreateInstance(serializerType); + return jsonSerializer; } } } diff --git a/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs index 763be879..d73e2cc6 100644 --- a/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs +++ b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs @@ -16,8 +16,16 @@ namespace GraphQL.Client.LocalExecution { - public class GraphQLLocalExecutionClient: IGraphQLClient where TSchema: ISchema - { + public static class GraphQLLocalExecutionClient { + public static GraphQLLocalExecutionClient New(TSchema schema) where TSchema : ISchema + => new GraphQLLocalExecutionClient(schema); + + public static GraphQLLocalExecutionClient New(TSchema schema, IGraphQLJsonSerializer serializer) where TSchema : ISchema + => new GraphQLLocalExecutionClient(schema, serializer); + } + + + public class GraphQLLocalExecutionClient: IGraphQLClient where TSchema: ISchema { private static readonly JsonSerializerSettings VariablesSerializerSettings = new JsonSerializerSettings { Formatting = Formatting.Indented, diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index 08afd595..62308cb8 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -15,13 +15,18 @@ public class NewtonsoftJsonSerializer: IGraphQLWebsocketJsonSerializer public NewtonsoftJsonSerializer() { Options = new NewtonsoftJsonSerializerOptions(); - } + } + public NewtonsoftJsonSerializer(Action configure) { + Options = new NewtonsoftJsonSerializerOptions(); + configure(Options); + } - public NewtonsoftJsonSerializer(NewtonsoftJsonSerializerOptions options) { + public NewtonsoftJsonSerializer(NewtonsoftJsonSerializerOptions options) { Options = options; } - public string SerializeToString(GraphQLRequest request) { + + public string SerializeToString(GraphQLRequest request) { return JsonConvert.SerializeObject(request, Options.JsonSerializerSettings); } diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index b59eb820..85339e6a 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -40,21 +40,21 @@ public GraphQLHttpClient(Action configure) { configure(Options); this.HttpClient = new HttpClient(Options.HttpMessageHandler); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); - JsonSerializer.EnsureAssigned(); + Options.JsonSerializer = JsonSerializer.EnsureAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options) { Options = options; this.HttpClient = new HttpClient(Options.HttpMessageHandler); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); - JsonSerializer.EnsureAssigned(); + Options.JsonSerializer = JsonSerializer.EnsureAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient) { Options = options; this.HttpClient = httpClient; this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); - JsonSerializer.EnsureAssigned(); + Options.JsonSerializer = JsonSerializer.EnsureAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient, IGraphQLWebsocketJsonSerializer serializer) { diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs new file mode 100644 index 00000000..4168ef79 --- /dev/null +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using FluentAssertions; +using GraphQL.Client.Abstractions; +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.LocalExecution; +using GraphQL.Client.Tests.Common; +using GraphQL.Client.Tests.Common.Chat; +using GraphQL.Client.Tests.Common.Chat.Schema; +using GraphQL.Client.Tests.Common.StarWars; +using Xunit; + +namespace GraphQL.Client.Serializer.Tests +{ + public abstract class BaseSerializerTest + { + public IGraphQLClient ChatClient { get; } + public IGraphQLClient StarWarsClient { get; } + + protected BaseSerializerTest(IGraphQLWebsocketJsonSerializer serializer) { + ChatClient = GraphQLLocalExecutionClient.New(Common.GetChatSchema(), serializer); + StarWarsClient = GraphQLLocalExecutionClient.New(Common.GetStarWarsSchema(), serializer); + } + + [Fact] + 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(); + response.Errors[0].Extensions.Should().NotBeNull(); + response.Errors[0].Extensions.Should().ContainKey("data"); + + AssertGraphQlErrorDataExtensions(response.Errors[0].Extensions["data"], ChatQuery.TestExtensions); + } + + /// + /// serializer-specific assertion of the field + /// + /// the field with key "data" from + /// + protected abstract void AssertGraphQlErrorDataExtensions(object dataField, IDictionary expectedContent); + + [Theory] + [ClassData(typeof(StarWarsHumans))] + public async void CanDoSerializationWithAnonymousTypes(int id, string name) { + var graphQLRequest = new GraphQLRequest(@" + query Human($id: String!){ + human(id: $id) { + name + } + } + + query Droid($id: String!) { + droid(id: $id) { + name + } + }", + new { id = id.ToString() }, + "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); + } + + [Fact] + public async void CanDoSerializationWithPredefinedTypes() { + const string message = "some random testing message"; + var response = await ChatClient.AddMessageAsync(message).ConfigureAwait(false); + + Assert.Equal(message, response.Data.AddMessage.Content); + } + } +} diff --git a/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj b/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj new file mode 100644 index 00000000..f84cef2c --- /dev/null +++ b/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + + + diff --git a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs new file mode 100644 index 00000000..9cbb7b4e --- /dev/null +++ b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using FluentAssertions; +using GraphQL.Client.Serializer.Newtonsoft; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace GraphQL.Client.Serializer.Tests { + public class NewtonsoftSerializerTest : BaseSerializerTest { + public NewtonsoftSerializerTest() : base(new NewtonsoftJsonSerializer()) { } + + protected override void AssertGraphQlErrorDataExtensions(object dataField, IDictionary expectedContent) { + dataField.Should().BeOfType().Which.Should().ContainKeys(expectedContent.Keys); + var data = (JObject) dataField; + + foreach (var item in expectedContent) { + switch (item.Value) { + case int i: + data[item.Key].Value().Should().Be(i); + break; + case string s: + data[item.Key].Value().Should().BeEquivalentTo(s); + break; + default: + Assert.True(false, $"unexpected value type \"{item.Value.GetType()}\" in expected content! Please review this unit test and add the missing type case!"); + break; + } + } + } + } +} diff --git a/tests/GraphQL.Client.Tests.Common/Chat/AddMessageMutationResult.cs b/tests/GraphQL.Client.Tests.Common/Chat/AddMessageMutationResult.cs new file mode 100644 index 00000000..e3aaaf1c --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Chat/AddMessageMutationResult.cs @@ -0,0 +1,9 @@ +namespace GraphQL.Client.Tests.Common.Chat +{ + public class AddMessageMutationResult { + public AddMessageContent AddMessage { get; set; } + public class AddMessageContent { + public string Content { get; set; } + } + } +} diff --git a/tests/GraphQL.Client.Tests.Common/Chat/AddMessageVariables.cs b/tests/GraphQL.Client.Tests.Common/Chat/AddMessageVariables.cs new file mode 100644 index 00000000..129c5c2d --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Chat/AddMessageVariables.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GraphQL.Client.Tests.Common.Chat { + public class AddMessageVariables { + + public AddMessageInput Input { get; set; } + public class AddMessageInput { + public string FromId { get; set; } + public string Content { get; set; } + public DateTime SentAt { get; set; } + } + } +} diff --git a/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs b/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs new file mode 100644 index 00000000..44319a17 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions; + +namespace GraphQL.Client.Tests.Common.Chat { + public static class GraphQLClientChatExtensions { + public static Task> AddMessageAsync(this IGraphQLClient client, string message) { + var graphQLRequest = new GraphQLRequest( + @"mutation($input: MessageInputType){ + addMessage(message: $input){ + content + } + }", + new { + input = new { + fromId = "2", + content = message, + sentAt = DateTime.Now + } + }); + return client.SendMutationAsync(graphQLRequest); + } + + public static Task> JoinDeveloperUser(this IGraphQLClient client) { + var graphQLRequest = new GraphQLRequest(@" + mutation($userId: String){ + join(userId: $userId){ + displayName + id + } + }", + new { + userId = "1" + }); + return client.SendMutationAsync(graphQLRequest); + } + + } +} diff --git a/tests/GraphQL.Client.Tests.Common/Chat/JoinDeveloperMutationResult.cs b/tests/GraphQL.Client.Tests.Common/Chat/JoinDeveloperMutationResult.cs new file mode 100644 index 00000000..2807a5b2 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Chat/JoinDeveloperMutationResult.cs @@ -0,0 +1,10 @@ +namespace GraphQL.Client.Tests.Common.Chat +{ + public class JoinDeveloperMutationResult { + public JoinContent Join { get; set; } + public class JoinContent { + public string DisplayName { get; set; } + public string Id { get; set; } + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/CapitalizedFieldsGraphType.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/CapitalizedFieldsGraphType.cs similarity index 58% rename from tests/IntegrationTestServer/ChatSchema/CapitalizedFieldsGraphType.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/CapitalizedFieldsGraphType.cs index 32b235e5..ddcd1238 100644 --- a/tests/IntegrationTestServer/ChatSchema/CapitalizedFieldsGraphType.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/CapitalizedFieldsGraphType.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Metadata.Ecma335; -using System.Threading.Tasks; using GraphQL.Types; -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public class CapitalizedFieldsGraphType: ObjectGraphType { public CapitalizedFieldsGraphType() { Name = "CapitalizedFields"; diff --git a/tests/IntegrationTestServer/ChatSchema/ChatMutation.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatMutation.cs similarity index 94% rename from tests/IntegrationTestServer/ChatSchema/ChatMutation.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatMutation.cs index 43895aec..384639ab 100644 --- a/tests/IntegrationTestServer/ChatSchema/ChatMutation.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatMutation.cs @@ -1,6 +1,6 @@ using GraphQL.Types; -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public class ChatMutation : ObjectGraphType { public ChatMutation(IChat chat) { Field("addMessage", diff --git a/tests/IntegrationTestServer/ChatSchema/ChatQuery.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs similarity index 91% rename from tests/IntegrationTestServer/ChatSchema/ChatQuery.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs index 8b423f95..32357338 100644 --- a/tests/IntegrationTestServer/ChatSchema/ChatQuery.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; using System.Linq; -using GraphQL; using GraphQL.Types; -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public class ChatQuery : ObjectGraphType { public static readonly Dictionary TestExtensions = new Dictionary { diff --git a/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatSchema.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatSchema.cs new file mode 100644 index 00000000..8ef13e43 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatSchema.cs @@ -0,0 +1,10 @@ +namespace GraphQL.Client.Tests.Common.Chat.Schema { + public class ChatSchema : Types.Schema { + public ChatSchema(IDependencyResolver resolver) + : base(resolver) { + Query = resolver.Resolve(); + Mutation = resolver.Resolve(); + Subscription = resolver.Resolve(); + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatSubscriptions.cs similarity index 97% rename from tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatSubscriptions.cs index 8f3c43ed..f7df7dd5 100644 --- a/tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatSubscriptions.cs @@ -2,13 +2,12 @@ using System.Linq; using System.Reactive.Linq; using System.Security.Claims; -using GraphQL; using GraphQL.Resolvers; using GraphQL.Server.Transports.Subscriptions.Abstractions; using GraphQL.Subscription; using GraphQL.Types; -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public class ChatSubscriptions : ObjectGraphType { private readonly IChat _chat; diff --git a/tests/IntegrationTestServer/ChatSchema/IChat.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs similarity index 97% rename from tests/IntegrationTestServer/ChatSchema/IChat.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs index ef1bd08f..2e31e8b4 100644 --- a/tests/IntegrationTestServer/ChatSchema/IChat.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs @@ -3,7 +3,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public interface IChat { ConcurrentStack AllMessages { get; } diff --git a/tests/IntegrationTestServer/ChatSchema/Message.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/Message.cs similarity index 79% rename from tests/IntegrationTestServer/ChatSchema/Message.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/Message.cs index 93451aaa..1b68c7e4 100644 --- a/tests/IntegrationTestServer/ChatSchema/Message.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/Message.cs @@ -1,6 +1,6 @@ using System; -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public class Message { public MessageFrom From { get; set; } diff --git a/tests/IntegrationTestServer/ChatSchema/MessageFrom.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageFrom.cs similarity index 67% rename from tests/IntegrationTestServer/ChatSchema/MessageFrom.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageFrom.cs index 8dff8852..b6bc81a4 100644 --- a/tests/IntegrationTestServer/ChatSchema/MessageFrom.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageFrom.cs @@ -1,4 +1,4 @@ -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public class MessageFrom { public string Id { get; set; } diff --git a/tests/IntegrationTestServer/ChatSchema/MessageFromType.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageFromType.cs similarity index 76% rename from tests/IntegrationTestServer/ChatSchema/MessageFromType.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageFromType.cs index da243ee9..4d82c69e 100644 --- a/tests/IntegrationTestServer/ChatSchema/MessageFromType.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageFromType.cs @@ -1,6 +1,6 @@ using GraphQL.Types; -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public class MessageFromType : ObjectGraphType { public MessageFromType() { Field(o => o.Id); diff --git a/tests/IntegrationTestServer/ChatSchema/MessageType.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageType.cs similarity index 88% rename from tests/IntegrationTestServer/ChatSchema/MessageType.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageType.cs index 6740189a..c5172cf3 100644 --- a/tests/IntegrationTestServer/ChatSchema/MessageType.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageType.cs @@ -1,6 +1,6 @@ using GraphQL.Types; -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public class MessageType : ObjectGraphType { public MessageType() { Field(o => o.Content); diff --git a/tests/IntegrationTestServer/ChatSchema/ReceivedMessage.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ReceivedMessage.cs similarity index 76% rename from tests/IntegrationTestServer/ChatSchema/ReceivedMessage.cs rename to tests/GraphQL.Client.Tests.Common/Chat/Schema/ReceivedMessage.cs index a3d9025b..ab1b05f8 100644 --- a/tests/IntegrationTestServer/ChatSchema/ReceivedMessage.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ReceivedMessage.cs @@ -1,6 +1,6 @@ using System; -namespace IntegrationTestServer.ChatSchema { +namespace GraphQL.Client.Tests.Common.Chat.Schema { public class ReceivedMessage { public string FromId { get; set; } diff --git a/tests/GraphQL.Client.Tests.Common/Common.cs b/tests/GraphQL.Client.Tests.Common/Common.cs new file mode 100644 index 00000000..e912d907 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Common.cs @@ -0,0 +1,46 @@ +using GraphQL.Client.Tests.Common.Chat.Schema; +using GraphQL.StarWars; +using GraphQL.StarWars.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace GraphQL.Client.Tests.Common +{ + public static class Common + { + public static StarWarsSchema GetStarWarsSchema() { + var services = new ServiceCollection(); + services.AddTransient(provider => new FuncDependencyResolver(provider.GetService)); + services.AddStarWarsSchema(); + return services.BuildServiceProvider().GetRequiredService(); + } + public static ChatSchema GetChatSchema() { + var services = new ServiceCollection(); + services.AddTransient(provider => new FuncDependencyResolver(provider.GetService)); + services.AddChatSchema(); + return services.BuildServiceProvider().GetRequiredService(); + } + + public static void AddStarWarsSchema(this IServiceCollection services) { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + + public static void AddChatSchema(this IServiceCollection services) { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + } +} diff --git a/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj new file mode 100644 index 00000000..d9db0515 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + diff --git a/tests/GraphQL.Integration.Tests/TestData/StarWarsHumans.cs b/tests/GraphQL.Client.Tests.Common/StarWars/StarWarsHumans.cs similarity index 87% rename from tests/GraphQL.Integration.Tests/TestData/StarWarsHumans.cs rename to tests/GraphQL.Client.Tests.Common/StarWars/StarWarsHumans.cs index 99312c8c..44a9f6bd 100644 --- a/tests/GraphQL.Integration.Tests/TestData/StarWarsHumans.cs +++ b/tests/GraphQL.Client.Tests.Common/StarWars/StarWarsHumans.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Collections.Generic; -namespace GraphQL.Integration.Tests.TestData { +namespace GraphQL.Client.Tests.Common.StarWars { public class StarWarsHumans: IEnumerable { public IEnumerator GetEnumerator() { yield return new object[] { 1, "Luke" }; diff --git a/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs b/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs index f698b247..93a8a4bc 100644 --- a/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs +++ b/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs @@ -2,9 +2,12 @@ using System.Threading.Tasks; using GraphQL.Client; using GraphQL.Client.Http; +using GraphQL.Client.Tests.Common.Chat; namespace GraphQL.Integration.Tests.Extensions { public static class GraphQLClientTestExtensions { + + public static Task> AddMessageAsync(this GraphQLHttpClient client, string message) { var graphQLRequest = new GraphQLRequest( @"mutation($input: MessageInputType){ @@ -12,13 +15,11 @@ public static Task> AddMessageAsync(th content } }", - new { - input = new { - fromId = "2", - content = message, - sentAt = DateTime.Now - } - }); + new AddMessageVariables { Input = { + FromId = "2", + Content = message, + SentAt = DateTime.Now + }}); return client.SendMutationAsync(graphQLRequest); } diff --git a/tests/GraphQL.Integration.Tests/ExtensionsTest.cs b/tests/GraphQL.Integration.Tests/ExtensionsTest.cs index 1ffaabd2..cdfb5622 100644 --- a/tests/GraphQL.Integration.Tests/ExtensionsTest.cs +++ b/tests/GraphQL.Integration.Tests/ExtensionsTest.cs @@ -1,37 +1,11 @@ -using System; -using System.Text.Json; -using FluentAssertions; -using GraphQL.Client.Abstractions; using GraphQL.Integration.Tests.Helpers; using IntegrationTestServer; -using IntegrationTestServer.ChatSchema; -using Newtonsoft.Json; -using Xunit; namespace GraphQL.Integration.Tests { public class ExtensionsTest { private static TestServerSetup SetupTest(bool requestsViaWebsocket = false) => WebHostHelpers.SetupTest(requestsViaWebsocket); - //[Fact] - //public async void CanDeserializeExtensions() { - - // using var setup = SetupTest(); - // var response = await setup.Client.SendQueryAsync(new GraphQLRequest("query { extensionsTest }"), - // () => new {extensionsTest = ""}) - // .ConfigureAwait(false); - - // response.Errors.Should().NotBeNull(); - // response.Errors.Should().ContainSingle(); - // response.Errors[0].Extensions.Should().NotBeNull(); - // response.Errors[0].Extensions.Should().ContainKey("data"); - - // foreach (var item in ChatQuery.TestExtensions) { - - - // } - //} - //[Fact] //public async void DontNeedToUseCamelCaseNamingStrategy() { diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs index d897884f..82826164 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs @@ -2,8 +2,8 @@ using System.Text.Json; using GraphQL.Client.Abstractions; using GraphQL.Client.Http; +using GraphQL.Client.Tests.Common.StarWars; using GraphQL.Integration.Tests.Helpers; -using GraphQL.Integration.Tests.TestData; using IntegrationTestServer; using Newtonsoft.Json.Linq; using Xunit; diff --git a/tests/IntegrationTestServer/ChatSchema/ChatSchema.cs b/tests/IntegrationTestServer/ChatSchema/ChatSchema.cs deleted file mode 100644 index 3878bcf5..00000000 --- a/tests/IntegrationTestServer/ChatSchema/ChatSchema.cs +++ /dev/null @@ -1,11 +0,0 @@ -using GraphQL.Types; - -namespace IntegrationTestServer.ChatSchema { - public class ChatSchema : Schema { - public ChatSchema(IChat chat) { - Query = new ChatQuery(chat); - Mutation = new ChatMutation(chat); - Subscription = new ChatSubscriptions(chat); - } - } -} diff --git a/tests/IntegrationTestServer/IntegrationTestServer.csproj b/tests/IntegrationTestServer/IntegrationTestServer.csproj index 6ecc6a43..a1f28376 100644 --- a/tests/IntegrationTestServer/IntegrationTestServer.csproj +++ b/tests/IntegrationTestServer/IntegrationTestServer.csproj @@ -16,8 +16,12 @@ - - + + + + + + diff --git a/tests/IntegrationTestServer/StartupChat.cs b/tests/IntegrationTestServer/StartupChat.cs index 37c205e7..2a782991 100644 --- a/tests/IntegrationTestServer/StartupChat.cs +++ b/tests/IntegrationTestServer/StartupChat.cs @@ -1,5 +1,6 @@ +using GraphQL.Client.Tests.Common; +using GraphQL.Client.Tests.Common.Chat.Schema; using GraphQL.Server; -using IntegrationTestServer.ChatSchema; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -10,18 +11,12 @@ public class StartupChat: Startup { public StartupChat(IConfiguration configuration, IWebHostEnvironment environment): base(configuration, environment) { } public override void ConfigureGraphQLSchemaServices(IServiceCollection services) { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddChatSchema(); } public override void ConfigureGraphQLSchema(IApplicationBuilder app) { - app.UseGraphQLWebSockets("/graphql"); - app.UseGraphQL("/graphql"); + app.UseGraphQLWebSockets("/graphql"); + app.UseGraphQL("/graphql"); } } } diff --git a/tests/IntegrationTestServer/StartupStarWars.cs b/tests/IntegrationTestServer/StartupStarWars.cs index c62dc5f2..15609515 100644 --- a/tests/IntegrationTestServer/StartupStarWars.cs +++ b/tests/IntegrationTestServer/StartupStarWars.cs @@ -1,6 +1,6 @@ +using GraphQL.Client.Tests.Common; using GraphQL.Server; using GraphQL.StarWars; -using GraphQL.StarWars.Types; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -11,16 +11,7 @@ public class StartupStarWars: Startup { public StartupStarWars(IConfiguration configuration, IWebHostEnvironment environment): base(configuration, environment) { } public override void ConfigureGraphQLSchemaServices(IServiceCollection services) { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddStarWarsSchema(); } public override void ConfigureGraphQLSchema(IApplicationBuilder app) { From 0c66960725ba402d4688a77f37901b7f1a0ddb8f Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 11 Feb 2020 19:29:14 +0100 Subject: [PATCH 09/55] homogenize stream consuming serialization methods --- .../IGraphQLWebsocketJsonSerializer.cs | 3 ++- .../GraphQL.Client.Serializer.Newtonsoft.csproj | 3 ++- .../NewtonsoftJsonSerializer.cs | 13 +++++++------ .../Websocket/GraphQLHttpWebSocket.cs | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs index 7a215911..e2f445f5 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace GraphQL.Client.Abstractions.Websocket { @@ -9,7 +10,7 @@ namespace GraphQL.Client.Abstractions.Websocket public interface IGraphQLWebsocketJsonSerializer: IGraphQLJsonSerializer { byte[] SerializeToBytes(GraphQLWebSocketRequest request); - WebsocketResponseWrapper DeserializeToWebsocketResponseWrapper(Stream stream); + Task DeserializeToWebsocketResponseWrapperAsync(Stream stream); GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes); } diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQL.Client.Serializer.Newtonsoft.csproj b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQL.Client.Serializer.Newtonsoft.csproj index 9ae19080..4c6e0e77 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQL.Client.Serializer.Newtonsoft.csproj +++ b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQL.Client.Serializer.Newtonsoft.csproj @@ -1,7 +1,8 @@ + - netstandard2.0;netstandard2.1 + netstandard2.0 8.0 diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index 62308cb8..269293c0 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -17,8 +17,9 @@ public NewtonsoftJsonSerializer() Options = new NewtonsoftJsonSerializerOptions(); } public NewtonsoftJsonSerializer(Action configure) { - Options = new NewtonsoftJsonSerializerOptions(); - configure(Options); + var options = new NewtonsoftJsonSerializerOptions(); + configure(options); + Options = options; } public NewtonsoftJsonSerializer(NewtonsoftJsonSerializerOptions options) { @@ -35,7 +36,7 @@ public byte[] SerializeToBytes(GraphQLWebSocketRequest request) { return Encoding.UTF8.GetBytes(json); } - public WebsocketResponseWrapper DeserializeToWebsocketResponseWrapper(Stream stream) { + public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { return DeserializeFromUtf8Stream(stream); } @@ -45,15 +46,15 @@ public GraphQLWebSocketResponse> DeserializeToWebsock } public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { - return Task.FromResult(DeserializeFromUtf8Stream>(stream)); + return DeserializeFromUtf8Stream>(stream); } - private T DeserializeFromUtf8Stream(Stream stream) { + private Task DeserializeFromUtf8Stream(Stream stream) { using StreamReader sr = new StreamReader(stream); using JsonReader reader = new JsonTextReader(sr); JsonSerializer serializer = JsonSerializer.Create(Options.JsonSerializerSettings); - return serializer.Deserialize(reader); + return Task.FromResult(serializer.Deserialize(reader)); } } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index d5bdcc04..579be9e7 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -223,7 +223,7 @@ private async Task _receiveResultAsync() { ms.Seek(0, SeekOrigin.Begin); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { - var response = _options.JsonSerializer.DeserializeToWebsocketResponseWrapper(ms); + var response = await _options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); response.MessageBytes = ms.ToArray(); return response; } From 6aba359f3ca46a7ecbabfbd6f436e3eea07393d6 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 11 Feb 2020 22:07:37 +0100 Subject: [PATCH 10/55] implement system.text.json serializer --- GraphQL.Client.sln | 13 +++- ...QL.Client.Serializer.SystemTextJson.csproj | 21 ++++++ .../GraphQLExtensionsConverter.cs | 73 +++++++++++++++++++ .../SystemTextJsonSerializer.cs | 49 +++++++++++++ .../SystemTextJsonSerializerOptions.cs | 23 ++++++ src/GraphQL.Primitives/GraphQLError.cs | 6 +- .../GraphQLExtensionsType.cs | 9 +++ src/GraphQL.Primitives/GraphQLResponse.cs | 6 +- .../GraphQL.Client.Serializer.Tests.csproj | 1 + .../SystemTextJsonSerializerTests.cs | 18 +++++ .../Extensions/GraphQLClientTestExtensions.cs | 14 ++-- .../Helpers/WebHostHelpers.cs | 4 +- 12 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj create mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/GraphQLExtensionsConverter.cs create mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs create mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs create mode 100644 src/GraphQL.Primitives/GraphQLExtensionsType.cs create mode 100644 tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln index df866b4e..510fa0b0 100644 --- a/GraphQL.Client.sln +++ b/GraphQL.Client.sln @@ -62,11 +62,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Abstractions EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Serializer.Newtonsoft", "src\GraphQL.Client.Serializer.Newtonsoft\GraphQL.Client.Serializer.Newtonsoft.csproj", "{11F28E78-ADE4-4153-B97C-56136EB7BD5B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.LocalExecution", "src\GraphQL.Client.LocalExecution\GraphQL.Client.LocalExecution.csproj", "{2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.LocalExecution", "src\GraphQL.Client.LocalExecution\GraphQL.Client.LocalExecution.csproj", "{2BEC821C-E405-43CB-9BC9-A6BB0322F6C2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.Serializer.Tests", "tests\GraphQL.Client.Serializer.Tests\GraphQL.Client.Serializer.Tests.csproj", "{CA842D18-FC4A-4281-B1FF-080FA91887B8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Serializer.Tests", "tests\GraphQL.Client.Serializer.Tests\GraphQL.Client.Serializer.Tests.csproj", "{CA842D18-FC4A-4281-B1FF-080FA91887B8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.Tests.Common", "tests\GraphQL.Client.Tests.Common\GraphQL.Client.Tests.Common.csproj", "{0D307BAD-27AE-4A5D-8764-4AA2620B01E9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Tests.Common", "tests\GraphQL.Client.Tests.Common\GraphQL.Client.Tests.Common.csproj", "{0D307BAD-27AE-4A5D-8764-4AA2620B01E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.Serializer.SystemTextJson", "src\GraphQL.Client.Serializer.SystemTextJson\GraphQL.Client.Serializer.SystemTextJson.csproj", "{7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -126,6 +128,10 @@ Global {0D307BAD-27AE-4A5D-8764-4AA2620B01E9}.Debug|Any CPU.Build.0 = Debug|Any CPU {0D307BAD-27AE-4A5D-8764-4AA2620B01E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D307BAD-27AE-4A5D-8764-4AA2620B01E9}.Release|Any CPU.Build.0 = Release|Any CPU + {7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -144,6 +150,7 @@ Global {2BEC821C-E405-43CB-9BC9-A6BB0322F6C2} = {47C98B55-08F1-4428-863E-2C5C876DEEFE} {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj new file mode 100644 index 00000000..024b0681 --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj @@ -0,0 +1,21 @@ + + + + + netstandard2.0;netcoreapp3.1 + 8.0 + + + + + + + + + + + + + + + diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLExtensionsConverter.cs b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLExtensionsConverter.cs new file mode 100644 index 00000000..e10c8635 --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLExtensionsConverter.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GraphQL.Client.Serializer.SystemTextJson { + /// + /// A custom JsonConverter for reading the extension fields of and . + /// + /// + /// Taken and modified from GraphQL.SystemTextJson.ObjectDictionaryConverter (GraphQL.NET) + /// + public class GraphQLExtensionsConverter : JsonConverter { + public override GraphQLExtensionsType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + using var doc = JsonDocument.ParseValue(ref reader); + + if (doc?.RootElement == null || doc?.RootElement.ValueKind != JsonValueKind.Object) { + throw new ArgumentException("This converter can only parse when the root element is a JSON Object."); + } + + return ReadDictionary(doc.RootElement); + } + + public override void Write(Utf8JsonWriter writer, GraphQLExtensionsType value, JsonSerializerOptions options) + => throw new NotImplementedException( + "This converter currently is only intended to be used to read a JSON object into a strongly-typed representation."); + + private TDictionary ReadDictionary(JsonElement element) where TDictionary: Dictionary { + var result = Activator.CreateInstance(); + foreach (var property in element.EnumerateObject()) { + result[property.Name] = ReadValue(property.Value); + } + return result; + } + + private IEnumerable ReadArray(JsonElement value) { + foreach (var item in value.EnumerateArray()) { + yield return ReadValue(item); + } + } + + private object ReadValue(JsonElement value) + => value.ValueKind switch + { + JsonValueKind.Array => ReadArray(value).ToList(), + JsonValueKind.Object => ReadDictionary>(value), + JsonValueKind.Number => ReadNumber(value), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.String => value.GetString(), + JsonValueKind.Null => null, + JsonValueKind.Undefined => null, + _ => throw new InvalidOperationException($"Unexpected value kind: {value.ValueKind}") + }; + + private object ReadNumber(JsonElement value) { + if (value.TryGetInt32(out var i)) + return i; + else if (value.TryGetInt64(out var l)) + return l; + else if (BigInteger.TryParse(value.GetRawText(), out var bi)) + return bi; + else if (value.TryGetDouble(out var d)) + return d; + else if (value.TryGetDecimal(out var dd)) + return dd; + + throw new NotImplementedException($"Unexpected Number value. Raw text was: {value.GetRawText()}"); + } + } +} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs new file mode 100644 index 00000000..a8cf7cde --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions.Websocket; + +namespace GraphQL.Client.Serializer.SystemTextJson +{ + public class SystemTextJsonSerializer: IGraphQLWebsocketJsonSerializer + { + public SystemTextJsonSerializerOptions Options { get; } + + public SystemTextJsonSerializer() + { + Options = new SystemTextJsonSerializerOptions(); + } + public SystemTextJsonSerializer(Action configure) { + var options = new SystemTextJsonSerializerOptions(); + configure(options); + Options = options; + } + + public SystemTextJsonSerializer(SystemTextJsonSerializerOptions options) { + Options = options; + } + + public string SerializeToString(GraphQLRequest request) { + return JsonSerializer.Serialize(request, Options.JsonSerializerOptions); + } + + public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { + return JsonSerializer.DeserializeAsync>(stream, Options.JsonSerializerOptions, cancellationToken).AsTask(); + } + + public byte[] SerializeToBytes(GraphQLWebSocketRequest request) { + return JsonSerializer.SerializeToUtf8Bytes(request, Options.JsonSerializerOptions); + } + + public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { + return JsonSerializer.DeserializeAsync(stream, Options.JsonSerializerOptions).AsTask(); + } + + public GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes) { + return JsonSerializer.Deserialize>>(new ReadOnlySpan(bytes), + Options.JsonSerializerOptions); + } + } +} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs new file mode 100644 index 00000000..64b65b6c --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs @@ -0,0 +1,23 @@ +using System.Text.Json; +using Dahomey.Json; + +namespace GraphQL.Client.Serializer.SystemTextJson { + public class SystemTextJsonSerializerOptions { + public JsonSerializerOptions JsonSerializerOptions { get; set; } = DefaultJsonSerializerOptions; + + public static JsonSerializerOptions DefaultJsonSerializerOptions { + get { + var options = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + options.Converters.Add(new GraphQLExtensionsConverter()); + + // Use extensions brought by Dahomey.Json to allow to deserialize anonymous types + options.SetupExtensions(); + + return options; + } + } + } +} diff --git a/src/GraphQL.Primitives/GraphQLError.cs b/src/GraphQL.Primitives/GraphQLError.cs index 7c0b0c62..3392e97c 100644 --- a/src/GraphQL.Primitives/GraphQLError.cs +++ b/src/GraphQL.Primitives/GraphQLError.cs @@ -32,7 +32,7 @@ public class GraphQLError : IEquatable { /// The extensions of the error /// [DataMember(Name = "extensions")] - public ExtensionsType? Extensions { get; set; } + public GraphQLExtensionsType? Extensions { get; set; } /// /// Returns a value that indicates whether this instance is equal to a specified object @@ -101,10 +101,6 @@ public override int GetHashCode() { public static bool operator !=(GraphQLError? left, GraphQLError? right) => !EqualityComparer.Default.Equals(left, right); - /// - /// The GraphQL extensions type. Create a custom json converter for this class to customize your serializers behaviour - /// - public class ExtensionsType : Dictionary { } } } diff --git a/src/GraphQL.Primitives/GraphQLExtensionsType.cs b/src/GraphQL.Primitives/GraphQLExtensionsType.cs new file mode 100644 index 00000000..2de718d0 --- /dev/null +++ b/src/GraphQL.Primitives/GraphQLExtensionsType.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace GraphQL { + + /// + /// The GraphQL extensions type. Create a custom json converter for this class to customize your serializers behaviour + /// + public class GraphQLExtensionsType: Dictionary { } +} diff --git a/src/GraphQL.Primitives/GraphQLResponse.cs b/src/GraphQL.Primitives/GraphQLResponse.cs index aa58ff77..088be7bd 100644 --- a/src/GraphQL.Primitives/GraphQLResponse.cs +++ b/src/GraphQL.Primitives/GraphQLResponse.cs @@ -14,7 +14,7 @@ public class GraphQLResponse : IEquatable?> { public GraphQLError[]? Errors { get; set; } [DataMember(Name = "extensions")] - public ExtensionsType? Extensions { get; set; } + public GraphQLExtensionsType? Extensions { get; set; } public override bool Equals(object? obj) => this.Equals(obj as GraphQLResponse); @@ -54,10 +54,6 @@ public override int GetHashCode() { public static bool operator !=(GraphQLResponse? response1, GraphQLResponse? response2) => !(response1 == response2); - /// - /// The GraphQL extensions type. Create a custom json converter for this class to customize your serializers behaviour - /// - public class ExtensionsType : Dictionary { } } diff --git a/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj b/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj index f84cef2c..583a8e82 100644 --- a/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj +++ b/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs new file mode 100644 index 00000000..5d54898a --- /dev/null +++ b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Text.Json; +using FluentAssertions; +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Serializer.SystemTextJson; +using Xunit; + +namespace GraphQL.Client.Serializer.Tests { + public class SystemTextJsonSerializerTests: BaseSerializerTest { + public SystemTextJsonSerializerTests() : base(new SystemTextJsonSerializer()) + { + } + + protected override void AssertGraphQlErrorDataExtensions(object dataField, IDictionary expectedContent) { + dataField.Should().BeEquivalentTo(expectedContent); + } + } +} diff --git a/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs b/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs index 93a8a4bc..59a6c0e3 100644 --- a/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs +++ b/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs @@ -9,17 +9,21 @@ public static class GraphQLClientTestExtensions { public static Task> AddMessageAsync(this GraphQLHttpClient client, string message) { + var variables = new AddMessageVariables { + Input = new AddMessageVariables.AddMessageInput{ + FromId = "2", + Content = message, + SentAt = DateTime.Now + } + }; + var graphQLRequest = new GraphQLRequest( @"mutation($input: MessageInputType){ addMessage(message: $input){ content } }", - new AddMessageVariables { Input = { - FromId = "2", - Content = message, - SentAt = DateTime.Now - }}); + variables); return client.SendMutationAsync(graphQLRequest); } diff --git a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs index 8c32f558..45c54392 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs @@ -1,6 +1,7 @@ using System; using GraphQL.Client; using GraphQL.Client.Http; +using GraphQL.Client.Serializer.Newtonsoft; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -34,7 +35,8 @@ public static IWebHost CreateServer(int port) where TStartup : class public static GraphQLHttpClient GetGraphQLClient(int port, bool requestsViaWebsocket = false) => new GraphQLHttpClient(new GraphQLHttpClientOptions { EndPoint = new Uri($"http://localhost:{port}/graphql"), - UseWebSocketForQueriesAndMutations = requestsViaWebsocket + UseWebSocketForQueriesAndMutations = requestsViaWebsocket, + JsonSerializer = new NewtonsoftJsonSerializer() }); public static TestServerSetup SetupTest(bool requestsViaWebsocket = false) where TStartup : class From 48249b2cdf383e637be5fc8349d2c7928ecab509 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 11 Feb 2020 22:46:48 +0100 Subject: [PATCH 11/55] remove netstandard2.1 target --- .../GraphQL.Client.Abstractions.Websocket.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj b/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj index 30202e85..8623791f 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj @@ -2,7 +2,7 @@ - netstandard2.0;netstandard2.1 + netstandard2.0 8.0 From 28053f31e3334558d06f2d86d99a586f83d981f2 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 11 Feb 2020 23:20:31 +0100 Subject: [PATCH 12/55] override request classes for system.text.json --- .../GraphQLWebSocketRequest.cs | 13 +++++++--- .../STJGraphQLRequest.cs | 21 ++++++++++++++++ .../STJGraphQLWebSocketRequest.cs | 24 +++++++++++++++++++ .../SystemTextJsonSerializer.cs | 5 ++-- src/GraphQL.Primitives/GraphQLRequest.cs | 15 +++++++----- 5 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs create mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLWebSocketRequest.cs diff --git a/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs index f43995e2..0f486482 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.Serialization; using System.Threading.Tasks; namespace GraphQL.Client.Abstractions.Websocket { @@ -8,21 +9,27 @@ namespace GraphQL.Client.Abstractions.Websocket { /// A Subscription Request /// public class GraphQLWebSocketRequest : IEquatable { + public const string IdKey = "id"; + public const string TypeKey = "type"; + public const string PayloadKey = "payload"; /// /// The Identifier of the Response /// - public string Id { get; set; } + [DataMember(Name = IdKey)] + public virtual string Id { get; set; } /// /// The Type of the Request /// - public string Type { get; set; } + [DataMember(Name = TypeKey)] + public virtual string Type { get; set; } /// /// The payload of the websocket request /// - public GraphQLRequest Payload { get; set; } + [DataMember(Name = PayloadKey)] + public virtual GraphQLRequest Payload { get; set; } private TaskCompletionSource _tcs = new TaskCompletionSource(); diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs new file mode 100644 index 00000000..ba44b66d --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace GraphQL.Client.Serializer.SystemTextJson { + public class STJGraphQLRequest: 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 STJGraphQLRequest() { } + + public STJGraphQLRequest(GraphQLRequest other) { + Query = other.Query; + OperationName = other.OperationName; + Variables = other.Variables; + } + + } +} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLWebSocketRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLWebSocketRequest.cs new file mode 100644 index 00000000..bf6adbeb --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLWebSocketRequest.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; +using GraphQL.Client.Abstractions.Websocket; + +namespace GraphQL.Client.Serializer.SystemTextJson { + public class STJGraphQLWebSocketRequest: GraphQLWebSocketRequest { + + [JsonPropertyName(IdKey)] + public override string Id { get; set; } + [JsonPropertyName(TypeKey)] + public override string Type { get; set; } + [JsonPropertyName(PayloadKey)] + public override GraphQLRequest Payload { get; set; } + + public STJGraphQLWebSocketRequest() + { + } + + public STJGraphQLWebSocketRequest(GraphQLWebSocketRequest other) { + Id = other.Id; + Type = other.Type; + Payload = new STJGraphQLRequest(other.Payload); + } + } +} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index a8cf7cde..599a402b 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -26,7 +26,7 @@ public SystemTextJsonSerializer(SystemTextJsonSerializerOptions options) { } public string SerializeToString(GraphQLRequest request) { - return JsonSerializer.Serialize(request, Options.JsonSerializerOptions); + return JsonSerializer.Serialize(new STJGraphQLRequest(request), Options.JsonSerializerOptions); } public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { @@ -34,7 +34,7 @@ public Task> DeserializeFromUtf8StreamAsync DeserializeToWebsocketResponseWrapperAsync(Stream stream) { @@ -45,5 +45,6 @@ public GraphQLWebSocketResponse> DeserializeToWebsock return JsonSerializer.Deserialize>>(new ReadOnlySpan(bytes), Options.JsonSerializerOptions); } + } } diff --git a/src/GraphQL.Primitives/GraphQLRequest.cs b/src/GraphQL.Primitives/GraphQLRequest.cs index d39d2cea..f7e48e24 100644 --- a/src/GraphQL.Primitives/GraphQLRequest.cs +++ b/src/GraphQL.Primitives/GraphQLRequest.cs @@ -8,24 +8,27 @@ namespace GraphQL { /// A GraphQL request /// public class GraphQLRequest : IEquatable { + public const string OperationNameKey = "operationName"; + public const string QueryKey = "query"; + public const string VariablesKey = "variables"; /// /// The Query /// - [DataMember(Name = "query")] - public string Query { get; set; } + [DataMember(Name = QueryKey)] + public virtual string Query { get; set; } /// /// The name of the Operation /// - [DataMember(Name = "operationName")] - public string? OperationName { get; set; } + [DataMember(Name = OperationNameKey)] + public virtual string? OperationName { get; set; } /// /// Represents the request variables /// - [DataMember(Name = "variables")] - public object? Variables { get; set; } + [DataMember(Name = VariablesKey)] + public virtual object? Variables { get; set; } public GraphQLRequest() { } From 98a53bfebdcbd56e9b98f6f8da4ecfdd4409dc5b Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 11 Feb 2020 23:42:49 +0100 Subject: [PATCH 13/55] add SerializeToStringTest theory (data incomplete) --- .../STJGraphQLRequest.cs | 1 + .../BaseSerializerTest.cs | 10 +++ .../TestData/SerializeToStringTestData.cs | 18 ++++++ .../Chat/GraphQLClientChatExtensions.cs | 29 +++++---- .../Extensions/GraphQLClientTestExtensions.cs | 61 ------------------- .../WebsocketTest.cs | 2 +- 6 files changed, 46 insertions(+), 75 deletions(-) create mode 100644 tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs delete mode 100644 tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs index ba44b66d..9ffa3afa 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Dahomey.Json.Attributes; namespace GraphQL.Client.Serializer.SystemTextJson { public class STJGraphQLRequest: GraphQLRequest { diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs index 4168ef79..2a0efef2 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs @@ -3,6 +3,7 @@ using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.LocalExecution; +using GraphQL.Client.Serializer.Tests.TestData; using GraphQL.Client.Tests.Common; using GraphQL.Client.Tests.Common.Chat; using GraphQL.Client.Tests.Common.Chat.Schema; @@ -13,14 +14,23 @@ namespace GraphQL.Client.Serializer.Tests { public abstract class BaseSerializerTest { + public IGraphQLWebsocketJsonSerializer Serializer { get; } public IGraphQLClient ChatClient { get; } public IGraphQLClient StarWarsClient { get; } protected BaseSerializerTest(IGraphQLWebsocketJsonSerializer serializer) { + Serializer = serializer; ChatClient = GraphQLLocalExecutionClient.New(Common.GetChatSchema(), serializer); StarWarsClient = GraphQLLocalExecutionClient.New(Common.GetStarWarsSchema(), serializer); } + [Theory] + [ClassData(typeof(SerializeToStringTestData))] + public void SerializeToStringTest(string expectedJson, GraphQLRequest request) { + var json = Serializer.SerializeToString(request); + json.Should().BeEquivalentTo(expectedJson); + } + [Fact] public async void CanDeserializeExtensions() { diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs new file mode 100644 index 00000000..5f4f18a1 --- /dev/null +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs @@ -0,0 +1,18 @@ +using System.Collections; +using System.Collections.Generic; +using GraphQL.Client.Tests.Common.Chat; + +namespace GraphQL.Client.Serializer.Tests.TestData { + public class SerializeToStringTestData : IEnumerable { + public IEnumerator GetEnumerator() { + yield return new object[] { + "{\"query\":\"simple query string\",\"operationName\":null,\"variables\":null}", + new GraphQLRequest("simple query string") + }; + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + } +} diff --git a/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs b/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs index 44319a17..d76aedcf 100644 --- a/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs @@ -4,20 +4,23 @@ namespace GraphQL.Client.Tests.Common.Chat { public static class GraphQLClientChatExtensions { + public const string AddMessageQuery = +@"mutation($input: MessageInputType){ + addMessage(message: $input){ + content + } +}"; + public static Task> AddMessageAsync(this IGraphQLClient client, string message) { - var graphQLRequest = new GraphQLRequest( - @"mutation($input: MessageInputType){ - addMessage(message: $input){ - content - } - }", - new { - input = new { - fromId = "2", - content = message, - sentAt = DateTime.Now - } - }); + var variables = new AddMessageVariables { + Input = new AddMessageVariables.AddMessageInput { + FromId = "2", + Content = message, + SentAt = DateTime.Now + } + }; + + var graphQLRequest = new GraphQLRequest(AddMessageQuery, variables); return client.SendMutationAsync(graphQLRequest); } diff --git a/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs b/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs deleted file mode 100644 index 59a6c0e3..00000000 --- a/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Threading.Tasks; -using GraphQL.Client; -using GraphQL.Client.Http; -using GraphQL.Client.Tests.Common.Chat; - -namespace GraphQL.Integration.Tests.Extensions { - public static class GraphQLClientTestExtensions { - - - public static Task> AddMessageAsync(this GraphQLHttpClient client, string message) { - var variables = new AddMessageVariables { - Input = new AddMessageVariables.AddMessageInput{ - FromId = "2", - Content = message, - SentAt = DateTime.Now - } - }; - - var graphQLRequest = new GraphQLRequest( - @"mutation($input: MessageInputType){ - addMessage(message: $input){ - content - } - }", - variables); - return client.SendMutationAsync(graphQLRequest); - } - - public static Task> JoinDeveloperUser(this GraphQLHttpClient client) { - var graphQLRequest = new GraphQLRequest(@" - mutation($userId: String){ - join(userId: $userId){ - displayName - id - } - }", - new { - userId = "1" - }); - return client.SendMutationAsync(graphQLRequest); - } - - } - - - public class AddMessageMutationResult { - public AddMessageContent AddMessage { get; set; } - public class AddMessageContent { - public string Content { get; set; } - } - } - - public class JoinDeveloperMutationResult { - public JoinContent Join { get; set; } - public class JoinContent { - public string DisplayName { get; set; } - public string Id { get; set; } - } - } -} diff --git a/tests/GraphQL.Integration.Tests/WebsocketTest.cs b/tests/GraphQL.Integration.Tests/WebsocketTest.cs index 4bd1fade..655aac5f 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTest.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTest.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; using GraphQL.Client.Abstractions; -using GraphQL.Integration.Tests.Extensions; +using GraphQL.Client.Tests.Common.Chat; using GraphQL.Integration.Tests.Helpers; using IntegrationTestServer; using Microsoft.AspNetCore.Hosting; From 82a0a315e08b0bba8975bc3137f752e19221e88d Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 12 Feb 2020 11:06:09 +0100 Subject: [PATCH 14/55] create converter for JSON.Net to achieve the same result as with system.text.json --- .../GraphQLExtensionsConverter.cs | 75 +++++++++++++++++++ .../NewtonsoftJsonSerializer.cs | 5 +- .../SystemTextJsonSerializer.cs | 9 ++- .../SystemTextJsonSerializerOptions.cs | 19 +---- .../BaseSerializerTest.cs | 8 +- .../NewtonsoftSerializerTest.cs | 23 ------ .../SystemTextJsonSerializerTests.cs | 9 --- .../Chat/GraphQLClientChatExtensions.cs | 2 +- 8 files changed, 90 insertions(+), 60 deletions(-) create mode 100644 src/GraphQL.Client.Serializer.Newtonsoft/GraphQLExtensionsConverter.cs diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLExtensionsConverter.cs b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLExtensionsConverter.cs new file mode 100644 index 00000000..0ff13a0f --- /dev/null +++ b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLExtensionsConverter.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace GraphQL.Client.Serializer.Newtonsoft { + public class GraphQLExtensionsConverter: JsonConverter { + public override void WriteJson(JsonWriter writer, GraphQLExtensionsType value, JsonSerializer serializer) { + throw new NotImplementedException( + "This converter currently is only intended to be used to read a JSON object into a strongly-typed representation."); + } + + public override GraphQLExtensionsType ReadJson(JsonReader reader, Type objectType, GraphQLExtensionsType existingValue, + bool hasExistingValue, JsonSerializer serializer) { + var rootToken = JToken.ReadFrom(reader); + if (rootToken is JObject) { + return ReadDictionary(rootToken); + } + else + throw new ArgumentException("This converter can only parse when the root element is a JSON Object."); + } + + private object ReadToken(JToken? token) { + switch (token.Type) { + case JTokenType.Undefined: + case JTokenType.None: + return null; + case JTokenType.Object: + return ReadDictionary>(token); + case JTokenType.Array: + return ReadArray(token); + case JTokenType.Integer: + return token.Value(); + case JTokenType.Float: + return token.Value(); + case JTokenType.Raw: + case JTokenType.String: + case JTokenType.Uri: + return token.Value(); + case JTokenType.Boolean: + return token.Value(); + case JTokenType.Date: + return token.Value(); + case JTokenType.Bytes: + return token.Value(); + case JTokenType.Guid: + return token.Value(); + case JTokenType.TimeSpan: + return token.Value(); + case JTokenType.Constructor: + case JTokenType.Property: + case JTokenType.Comment: + default: + throw new ArgumentOutOfRangeException(); + } + } + + private TDictionary ReadDictionary(JToken element) where TDictionary : Dictionary { + var result = Activator.CreateInstance(); + foreach (var property in ((JObject)element).Properties()) { + result[property.Name] = ReadToken(property.Value); + } + return result; + } + + private IEnumerable ReadArray(JToken element) { + foreach (var item in element.Values()) { + yield return ReadToken(item); + } + } + } +} diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index 269293c0..fad57216 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -15,16 +15,19 @@ public class NewtonsoftJsonSerializer: IGraphQLWebsocketJsonSerializer public NewtonsoftJsonSerializer() { Options = new NewtonsoftJsonSerializerOptions(); + Options.JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); } public NewtonsoftJsonSerializer(Action configure) { var options = new NewtonsoftJsonSerializerOptions(); configure(options); Options = options; + Options.JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); } public NewtonsoftJsonSerializer(NewtonsoftJsonSerializerOptions options) { Options = options; - } + Options.JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); + } public string SerializeToString(GraphQLRequest request) { diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index 599a402b..d4ab07d3 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -14,16 +14,19 @@ public class SystemTextJsonSerializer: IGraphQLWebsocketJsonSerializer public SystemTextJsonSerializer() { Options = new SystemTextJsonSerializerOptions(); - } + Options.JsonSerializerOptions.Converters.Insert(0, new GraphQLExtensionsConverter()); + } public SystemTextJsonSerializer(Action configure) { var options = new SystemTextJsonSerializerOptions(); configure(options); Options = options; - } + Options.JsonSerializerOptions.Converters.Insert(0, new GraphQLExtensionsConverter()); + } public SystemTextJsonSerializer(SystemTextJsonSerializerOptions options) { Options = options; - } + Options.JsonSerializerOptions.Converters.Insert(0, new GraphQLExtensionsConverter()); + } public string SerializeToString(GraphQLRequest request) { return JsonSerializer.Serialize(new STJGraphQLRequest(request), Options.JsonSerializerOptions); diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs index 64b65b6c..af9bbc35 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs @@ -3,21 +3,8 @@ namespace GraphQL.Client.Serializer.SystemTextJson { public class SystemTextJsonSerializerOptions { - public JsonSerializerOptions JsonSerializerOptions { get; set; } = DefaultJsonSerializerOptions; - - public static JsonSerializerOptions DefaultJsonSerializerOptions { - get { - var options = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - options.Converters.Add(new GraphQLExtensionsConverter()); - - // Use extensions brought by Dahomey.Json to allow to deserialize anonymous types - options.SetupExtensions(); - - return options; - } - } + public JsonSerializerOptions JsonSerializerOptions { get; set; } = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }.SetupExtensions(); } } diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs index 2a0efef2..c5275342 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs @@ -43,15 +43,9 @@ public async void CanDeserializeExtensions() { response.Errors[0].Extensions.Should().NotBeNull(); response.Errors[0].Extensions.Should().ContainKey("data"); - AssertGraphQlErrorDataExtensions(response.Errors[0].Extensions["data"], ChatQuery.TestExtensions); + response.Errors[0].Extensions["data"].Should().BeEquivalentTo(ChatQuery.TestExtensions); } - /// - /// serializer-specific assertion of the field - /// - /// the field with key "data" from - /// - protected abstract void AssertGraphQlErrorDataExtensions(object dataField, IDictionary expectedContent); [Theory] [ClassData(typeof(StarWarsHumans))] diff --git a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs index 9cbb7b4e..8a4196a6 100644 --- a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs @@ -1,30 +1,7 @@ -using System.Collections.Generic; -using FluentAssertions; using GraphQL.Client.Serializer.Newtonsoft; -using Newtonsoft.Json.Linq; -using Xunit; namespace GraphQL.Client.Serializer.Tests { public class NewtonsoftSerializerTest : BaseSerializerTest { public NewtonsoftSerializerTest() : base(new NewtonsoftJsonSerializer()) { } - - protected override void AssertGraphQlErrorDataExtensions(object dataField, IDictionary expectedContent) { - dataField.Should().BeOfType().Which.Should().ContainKeys(expectedContent.Keys); - var data = (JObject) dataField; - - foreach (var item in expectedContent) { - switch (item.Value) { - case int i: - data[item.Key].Value().Should().Be(i); - break; - case string s: - data[item.Key].Value().Should().BeEquivalentTo(s); - break; - default: - Assert.True(false, $"unexpected value type \"{item.Value.GetType()}\" in expected content! Please review this unit test and add the missing type case!"); - break; - } - } - } } } diff --git a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs index 5d54898a..177bb64b 100644 --- a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs +++ b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs @@ -1,18 +1,9 @@ -using System.Collections.Generic; -using System.Text.Json; -using FluentAssertions; -using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Serializer.SystemTextJson; -using Xunit; namespace GraphQL.Client.Serializer.Tests { public class SystemTextJsonSerializerTests: BaseSerializerTest { public SystemTextJsonSerializerTests() : base(new SystemTextJsonSerializer()) { } - - protected override void AssertGraphQlErrorDataExtensions(object dataField, IDictionary expectedContent) { - dataField.Should().BeEquivalentTo(expectedContent); - } } } diff --git a/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs b/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs index d76aedcf..f3d95cf7 100644 --- a/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs @@ -8,7 +8,7 @@ public static class GraphQLClientChatExtensions { @"mutation($input: MessageInputType){ addMessage(message: $input){ content - } + } }"; public static Task> AddMessageAsync(this IGraphQLClient client, string message) { From 949afcaf485884e00c5318d84e671a7f136be347 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 12 Feb 2020 11:32:39 +0100 Subject: [PATCH 15/55] consolidate constructors, add mandatory serializer settings in constructors --- .../GraphQLJsonSerializerExtensions.cs | 7 ++++ .../NewtonsoftJsonSerializer.cs | 22 ++++++------- .../SystemTextJsonSerializer.cs | 24 +++++++------- src/GraphQL.Client/GraphQLHttpClient.cs | 32 +++++++++---------- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs b/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs index b652c782..e544e75f 100644 --- a/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs +++ b/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; namespace GraphQL.Client.Abstractions { @@ -20,5 +21,11 @@ public static TSerializerInterface EnsureAssigned(this TSe return jsonSerializer; } + + public static TOptions New(this Action configure) { + var options = Activator.CreateInstance(); + configure(options); + return options; + } } } diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index fad57216..7115c763 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -1,8 +1,10 @@ using System; using System.IO; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; using Newtonsoft.Json; @@ -12,23 +14,19 @@ public class NewtonsoftJsonSerializer: IGraphQLWebsocketJsonSerializer { public NewtonsoftJsonSerializerOptions Options { get; } - public NewtonsoftJsonSerializer() - { - Options = new NewtonsoftJsonSerializerOptions(); - Options.JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); - } - public NewtonsoftJsonSerializer(Action configure) { - var options = new NewtonsoftJsonSerializerOptions(); - configure(options); - Options = options; - Options.JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); - } + public NewtonsoftJsonSerializer() : this(new NewtonsoftJsonSerializerOptions()) { } + + public NewtonsoftJsonSerializer(Action configure) : this(configure.New()) { } public NewtonsoftJsonSerializer(NewtonsoftJsonSerializerOptions options) { Options = options; - Options.JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); + ConfigureMandatorySerializerOptions(); } + private void ConfigureMandatorySerializerOptions() { + // deserialize extensions to Dictionary + Options.JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); + } public string SerializeToString(GraphQLRequest request) { return JsonConvert.SerializeObject(request, Options.JsonSerializerSettings); diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index d4ab07d3..f2b793d1 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; namespace GraphQL.Client.Serializer.SystemTextJson @@ -11,21 +12,20 @@ public class SystemTextJsonSerializer: IGraphQLWebsocketJsonSerializer { public SystemTextJsonSerializerOptions Options { get; } - public SystemTextJsonSerializer() - { - Options = new SystemTextJsonSerializerOptions(); - Options.JsonSerializerOptions.Converters.Insert(0, new GraphQLExtensionsConverter()); - } - public SystemTextJsonSerializer(Action configure) { - var options = new SystemTextJsonSerializerOptions(); - configure(options); - Options = options; - Options.JsonSerializerOptions.Converters.Insert(0, new GraphQLExtensionsConverter()); - } + public SystemTextJsonSerializer() : this(new SystemTextJsonSerializerOptions()) { } + + public SystemTextJsonSerializer(Action configure) : this(configure.New()) { } public SystemTextJsonSerializer(SystemTextJsonSerializerOptions options) { Options = options; - Options.JsonSerializerOptions.Converters.Insert(0, new GraphQLExtensionsConverter()); + ConfigureMandatorySerializerOptions(); + } + + private void ConfigureMandatorySerializerOptions() { + // deserialize extensions to Dictionary + Options.JsonSerializerOptions.Converters.Insert(0, new GraphQLExtensionsConverter()); + // allow the JSON field "data" to match the property "Data" even without JsonNamingPolicy.CamelCase + Options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; } public string SerializeToString(GraphQLRequest request) { diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 85339e6a..465a0286 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -28,42 +28,40 @@ public class GraphQLHttpClient : IGraphQLClient { /// public GraphQLHttpClientOptions Options { get; } - /// + /// + /// Publishes all exceptions which occur inside the websocket receive stream (i.e. for logging purposes) + /// public IObservable WebSocketReceiveErrors => graphQlHttpWebSocket.ReceiveErrors; + + #region Constructors + public GraphQLHttpClient(string endPoint) : this(new Uri(endPoint)) { } public GraphQLHttpClient(Uri endPoint) : this(o => o.EndPoint = endPoint) { } - public GraphQLHttpClient(Action configure) { - Options = new GraphQLHttpClientOptions(); - configure(Options); - this.HttpClient = new HttpClient(Options.HttpMessageHandler); - this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); - Options.JsonSerializer = JsonSerializer.EnsureAssigned(); - } + public GraphQLHttpClient(Action configure) : this(configure.New()) { } - public GraphQLHttpClient(GraphQLHttpClientOptions options) { - Options = options; - this.HttpClient = new HttpClient(Options.HttpMessageHandler); - this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); - Options.JsonSerializer = JsonSerializer.EnsureAssigned(); - } + public GraphQLHttpClient(GraphQLHttpClientOptions options) : this(options, new HttpClient(options.HttpMessageHandler)) { } public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient) { Options = options; - this.HttpClient = httpClient; + this.HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); Options.JsonSerializer = JsonSerializer.EnsureAssigned(); } public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient, IGraphQLWebsocketJsonSerializer serializer) { - Options = options; + Options = options ?? throw new ArgumentNullException(nameof(options)); Options.JsonSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); this.HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); } + #endregion + + #region IGraphQLClient + /// public Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { return Options.UseWebSocketForQueriesAndMutations @@ -107,6 +105,8 @@ public IObservable> CreateSubscriptionStream /// explicitly opens the websocket connection. Will be closed again on disposing the last subscription /// From 25459289c5e70fffe33a05c510b6f67ee686a9be Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 12 Feb 2020 12:25:50 +0100 Subject: [PATCH 16/55] test basic functionality of serializer without camelCase --- .../BaseSerializeNoCamelCaseTest.cs | 56 +++++++++++++++++++ .../BaseSerializerTest.cs | 5 +- .../NewtonsoftSerializerTest.cs | 6 ++ .../SystemTextJsonSerializerTests.cs | 11 +++- .../TestData/SerializeToStringTestData.cs | 6 +- .../GraphQL.Client.Tests.Common.csproj | 1 + .../Helpers/CallbackTester.cs | 2 +- .../Helpers/MiscellaneousExtensions.cs | 11 ++++ .../Helpers/NetworkHelpers.cs | 2 +- .../Helpers/ObservableTester.cs | 2 +- .../Helpers/WebHostHelpers.cs | 1 + .../QueryAndMutationTests.cs | 1 + .../WebsocketTest.cs | 1 + 13 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs rename tests/{GraphQL.Integration.Tests => GraphQL.Client.Tests.Common}/Helpers/CallbackTester.cs (97%) create mode 100644 tests/GraphQL.Client.Tests.Common/Helpers/MiscellaneousExtensions.cs rename tests/{GraphQL.Integration.Tests => GraphQL.Client.Tests.Common}/Helpers/NetworkHelpers.cs (85%) rename tests/{GraphQL.Integration.Tests => GraphQL.Client.Tests.Common}/Helpers/ObservableTester.cs (98%) diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs new file mode 100644 index 00000000..dbf11f52 --- /dev/null +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Text; +using FluentAssertions; +using GraphQL.Client.Abstractions; +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.LocalExecution; +using GraphQL.Client.Serializer.Tests.TestData; +using GraphQL.Client.Tests.Common; +using GraphQL.Client.Tests.Common.Helpers; +using Newtonsoft.Json; +using Xunit; + +namespace GraphQL.Client.Serializer.Tests { + public abstract class BaseSerializeNoCamelCaseTest { + + public IGraphQLWebsocketJsonSerializer Serializer { get; } + public IGraphQLClient ChatClient { get; } + public IGraphQLClient StarWarsClient { get; } + + protected BaseSerializeNoCamelCaseTest(IGraphQLWebsocketJsonSerializer serializer) { + Serializer = serializer; + ChatClient = GraphQLLocalExecutionClient.New(Common.GetChatSchema(), serializer); + StarWarsClient = GraphQLLocalExecutionClient.New(Common.GetStarWarsSchema(), serializer); + } + + [Theory] + [ClassData(typeof(SerializeToStringTestData))] + public void SerializeToStringTest(string expectedJson, GraphQLRequest request) { + var json = Serializer.SerializeToString(request).RemoveWhitespace(); + json.Should().BeEquivalentTo(expectedJson.RemoveWhitespace()); + } + + [Fact] + public async void WorksWithoutCamelCaseNamingStrategy() { + + const string message = "some random testing message"; + var graphQLRequest = new GraphQLRequest( + @"mutation($input: MessageInputType){ + addMessage(message: $input){ + content + } + }", + new { + input = new { + fromId = "2", + content = message, + sentAt = DateTime.Now + } + }); + var response = await ChatClient.SendMutationAsync(graphQLRequest, () => new { addMessage = new { content = "" } }); + + Assert.Equal(message, response.Data.addMessage.content); + } + } +} diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs index c5275342..92226813 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs @@ -7,6 +7,7 @@ using GraphQL.Client.Tests.Common; using GraphQL.Client.Tests.Common.Chat; using GraphQL.Client.Tests.Common.Chat.Schema; +using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Client.Tests.Common.StarWars; using Xunit; @@ -27,8 +28,8 @@ protected BaseSerializerTest(IGraphQLWebsocketJsonSerializer serializer) { [Theory] [ClassData(typeof(SerializeToStringTestData))] public void SerializeToStringTest(string expectedJson, GraphQLRequest request) { - var json = Serializer.SerializeToString(request); - json.Should().BeEquivalentTo(expectedJson); + var json = Serializer.SerializeToString(request).RemoveWhitespace(); + json.Should().BeEquivalentTo(expectedJson.RemoveWhitespace()); } [Fact] diff --git a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs index 8a4196a6..5e77e74d 100644 --- a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs @@ -1,7 +1,13 @@ using GraphQL.Client.Serializer.Newtonsoft; +using Newtonsoft.Json; namespace GraphQL.Client.Serializer.Tests { public class NewtonsoftSerializerTest : BaseSerializerTest { public NewtonsoftSerializerTest() : base(new NewtonsoftJsonSerializer()) { } } + + public class NewtonsoftSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest { + public NewtonsoftSerializeNoCamelCaseTest() + : base(new NewtonsoftJsonSerializer(options => options.JsonSerializerSettings = new JsonSerializerSettings())) { } + } } diff --git a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs index 177bb64b..c0bec30f 100644 --- a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs +++ b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs @@ -1,9 +1,14 @@ +using System.Text.Json; +using Dahomey.Json; using GraphQL.Client.Serializer.SystemTextJson; namespace GraphQL.Client.Serializer.Tests { public class SystemTextJsonSerializerTests: BaseSerializerTest { - public SystemTextJsonSerializerTests() : base(new SystemTextJsonSerializer()) - { - } + public SystemTextJsonSerializerTests() : base(new SystemTextJsonSerializer()) { } + } + + public class SystemTextJsonSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest { + public SystemTextJsonSerializeNoCamelCaseTest() + : base(new SystemTextJsonSerializer(options => options.JsonSerializerOptions = new JsonSerializerOptions().SetupExtensions())) { } } } diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs index 5f4f18a1..fed5d193 100644 --- a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs @@ -6,9 +6,13 @@ namespace GraphQL.Client.Serializer.Tests.TestData { public class SerializeToStringTestData : IEnumerable { public IEnumerator GetEnumerator() { yield return new object[] { - "{\"query\":\"simple query string\",\"operationName\":null,\"variables\":null}", + "{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":null}", new GraphQLRequest("simple query string") }; + yield return new object[] { + "{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":{\"camelCaseProperty\":\"test\",\"pascalCaseProperty\":\"test\"}}", + new GraphQLRequest("simple query string", new { camelCaseProperty = "test", PascalCaseProperty = "test"}) + }; } IEnumerator IEnumerable.GetEnumerator() { diff --git a/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj index d9db0515..3919581f 100644 --- a/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj +++ b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/GraphQL.Integration.Tests/Helpers/CallbackTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs similarity index 97% rename from tests/GraphQL.Integration.Tests/Helpers/CallbackTester.cs rename to tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs index e0d46b5f..7525d2b5 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/CallbackTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs @@ -2,7 +2,7 @@ using System.Threading; using Xunit; -namespace GraphQL.Integration.Tests.Helpers { +namespace GraphQL.Client.Tests.Common.Helpers { public class CallbackTester { private ManualResetEventSlim _callbackInvoked { get; } = new ManualResetEventSlim(); diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/MiscellaneousExtensions.cs b/tests/GraphQL.Client.Tests.Common/Helpers/MiscellaneousExtensions.cs new file mode 100644 index 00000000..2da34009 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Helpers/MiscellaneousExtensions.cs @@ -0,0 +1,11 @@ +using System.Linq; + +namespace GraphQL.Client.Tests.Common.Helpers { + public static class MiscellaneousExtensions { + public static string RemoveWhitespace(this string input) { + return new string(input.ToCharArray() + .Where(c => !char.IsWhiteSpace(c)) + .ToArray()); + } + } +} diff --git a/tests/GraphQL.Integration.Tests/Helpers/NetworkHelpers.cs b/tests/GraphQL.Client.Tests.Common/Helpers/NetworkHelpers.cs similarity index 85% rename from tests/GraphQL.Integration.Tests/Helpers/NetworkHelpers.cs rename to tests/GraphQL.Client.Tests.Common/Helpers/NetworkHelpers.cs index 6995ee54..ec08de02 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/NetworkHelpers.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/NetworkHelpers.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Sockets; -namespace GraphQL.Integration.Tests.Helpers { +namespace GraphQL.Client.Tests.Common.Helpers { public static class NetworkHelpers { public static int GetFreeTcpPortNumber() { var l = new TcpListener(IPAddress.Loopback, 0); diff --git a/tests/GraphQL.Integration.Tests/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs similarity index 98% rename from tests/GraphQL.Integration.Tests/Helpers/ObservableTester.cs rename to tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index a650237b..55c63c51 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -2,7 +2,7 @@ using System.Threading; using Xunit; -namespace GraphQL.Integration.Tests.Helpers { +namespace GraphQL.Client.Tests.Common.Helpers { public class ObservableTester : IDisposable { private readonly IDisposable _subscription; private ManualResetEventSlim _updateReceived { get; } = new ManualResetEventSlim(); diff --git a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs index 45c54392..7bb87cde 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs @@ -2,6 +2,7 @@ using GraphQL.Client; using GraphQL.Client.Http; using GraphQL.Client.Serializer.Newtonsoft; +using GraphQL.Client.Tests.Common.Helpers; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs index 82826164..fa6e301e 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs @@ -2,6 +2,7 @@ using System.Text.Json; using GraphQL.Client.Abstractions; using GraphQL.Client.Http; +using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Client.Tests.Common.StarWars; using GraphQL.Integration.Tests.Helpers; using IntegrationTestServer; diff --git a/tests/GraphQL.Integration.Tests/WebsocketTest.cs b/tests/GraphQL.Integration.Tests/WebsocketTest.cs index 655aac5f..baf7569c 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTest.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTest.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using GraphQL.Client.Abstractions; using GraphQL.Client.Tests.Common.Chat; +using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; using IntegrationTestServer; using Microsoft.AspNetCore.Hosting; From e2402cb4801fb68bd0bcb52a2a68c520d9aee528 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 12 Feb 2020 12:56:36 +0100 Subject: [PATCH 17/55] fix naming for NewtonSoft --- .../GraphQLRequest.cs | 21 +++++++++++++++++ .../GraphQLWebSocketRequest.cs | 23 +++++++++++++++++++ .../NewtonsoftJsonSerializer.cs | 6 ++--- .../NewtonsoftJsonSerializerOptions.cs | 2 +- ...STJGraphQLRequest.cs => GraphQLRequest.cs} | 7 +++--- ...tRequest.cs => GraphQLWebSocketRequest.cs} | 11 ++++----- .../SystemTextJsonSerializer.cs | 8 +++---- .../BaseSerializeNoCamelCaseTest.cs | 2 +- .../TestData/SerializeToStringTestData.cs | 4 ++-- 9 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs create mode 100644 src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs rename src/GraphQL.Client.Serializer.SystemTextJson/{STJGraphQLRequest.cs => GraphQLRequest.cs} (73%) rename src/GraphQL.Client.Serializer.SystemTextJson/{STJGraphQLWebSocketRequest.cs => GraphQLWebSocketRequest.cs} (50%) diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs new file mode 100644 index 00000000..cf012c92 --- /dev/null +++ b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..e5fa6999 --- /dev/null +++ b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs @@ -0,0 +1,23 @@ +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 = new Newtonsoft.GraphQLRequest(other.Payload); + } + } +} diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index 7115c763..f7ec6269 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -28,11 +28,11 @@ private void ConfigureMandatorySerializerOptions() { Options.JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); } - public string SerializeToString(GraphQLRequest request) { - return JsonConvert.SerializeObject(request, Options.JsonSerializerSettings); + public string SerializeToString(GraphQL.GraphQLRequest request) { + return JsonConvert.SerializeObject(new GraphQLRequest(request), Options.JsonSerializerSettings); } - public byte[] SerializeToBytes(GraphQLWebSocketRequest request) { + public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest request) { var json = JsonConvert.SerializeObject(request, Options.JsonSerializerSettings); return Encoding.UTF8.GetBytes(json); } diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs index 9148ed2e..d05f1dda 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs @@ -4,7 +4,7 @@ namespace GraphQL.Client.Serializer.Newtonsoft { public class NewtonsoftJsonSerializerOptions { public JsonSerializerSettings JsonSerializerSettings { get; set; } = new JsonSerializerSettings { - ContractResolver = new CamelCasePropertyNamesContractResolver() + ContractResolver = new CamelCasePropertyNamesContractResolver { IgnoreIsSpecifiedMembers = true } }; } } diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs similarity index 73% rename from src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs rename to src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs index 9ffa3afa..1f3b7908 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLRequest.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs @@ -1,8 +1,7 @@ using System.Text.Json.Serialization; -using Dahomey.Json.Attributes; namespace GraphQL.Client.Serializer.SystemTextJson { - public class STJGraphQLRequest: GraphQLRequest { + public class GraphQLRequest: GraphQL.GraphQLRequest { [JsonPropertyName(QueryKey)] public override string Query { get; set; } [JsonPropertyName(OperationNameKey)] @@ -10,9 +9,9 @@ public class STJGraphQLRequest: GraphQLRequest { [JsonPropertyName(VariablesKey)] public override object? Variables { get; set; } - public STJGraphQLRequest() { } + public GraphQLRequest() { } - public STJGraphQLRequest(GraphQLRequest other) { + public GraphQLRequest(GraphQL.GraphQLRequest other) { Query = other.Query; OperationName = other.OperationName; Variables = other.Variables; diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLWebSocketRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs similarity index 50% rename from src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLWebSocketRequest.cs rename to src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs index bf6adbeb..f7599391 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/STJGraphQLWebSocketRequest.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs @@ -1,24 +1,23 @@ using System.Text.Json.Serialization; -using GraphQL.Client.Abstractions.Websocket; namespace GraphQL.Client.Serializer.SystemTextJson { - public class STJGraphQLWebSocketRequest: GraphQLWebSocketRequest { + 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 GraphQLRequest Payload { get; set; } + public override GraphQL.GraphQLRequest Payload { get; set; } - public STJGraphQLWebSocketRequest() + public GraphQLWebSocketRequest() { } - public STJGraphQLWebSocketRequest(GraphQLWebSocketRequest other) { + public GraphQLWebSocketRequest(Abstractions.Websocket.GraphQLWebSocketRequest other) { Id = other.Id; Type = other.Type; - Payload = new STJGraphQLRequest(other.Payload); + Payload = new GraphQLRequest(other.Payload); } } } diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index f2b793d1..51f7b796 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -28,16 +28,16 @@ private void ConfigureMandatorySerializerOptions() { Options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; } - public string SerializeToString(GraphQLRequest request) { - return JsonSerializer.Serialize(new STJGraphQLRequest(request), Options.JsonSerializerOptions); + public string SerializeToString(GraphQL.GraphQLRequest request) { + return JsonSerializer.Serialize(new GraphQLRequest(request), Options.JsonSerializerOptions); } public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { return JsonSerializer.DeserializeAsync>(stream, Options.JsonSerializerOptions, cancellationToken).AsTask(); } - public byte[] SerializeToBytes(GraphQLWebSocketRequest request) { - return JsonSerializer.SerializeToUtf8Bytes(new STJGraphQLWebSocketRequest(request), Options.JsonSerializerOptions); + public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest request) { + return JsonSerializer.SerializeToUtf8Bytes(new GraphQLWebSocketRequest(request), Options.JsonSerializerOptions); } public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs index dbf11f52..833334b7 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs @@ -28,7 +28,7 @@ protected BaseSerializeNoCamelCaseTest(IGraphQLWebsocketJsonSerializer serialize [ClassData(typeof(SerializeToStringTestData))] public void SerializeToStringTest(string expectedJson, GraphQLRequest request) { var json = Serializer.SerializeToString(request).RemoveWhitespace(); - json.Should().BeEquivalentTo(expectedJson.RemoveWhitespace()); + json.Should().Be(expectedJson.RemoveWhitespace()); } [Fact] diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs index fed5d193..86d1ae9f 100644 --- a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs @@ -10,8 +10,8 @@ public IEnumerator GetEnumerator() { new GraphQLRequest("simple query string") }; yield return new object[] { - "{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":{\"camelCaseProperty\":\"test\",\"pascalCaseProperty\":\"test\"}}", - new GraphQLRequest("simple query string", new { camelCaseProperty = "test", PascalCaseProperty = "test"}) + "{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"}}", + new GraphQLRequest("simple query string", new { camelCaseProperty = "camelCase", PascalCaseProperty = "PascalCase"}) }; } From 4557e27be59b94690879f85fd9c62536c4631596 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 12 Feb 2020 13:13:09 +0100 Subject: [PATCH 18/55] test websocket serialization --- .../GraphQLWebSocketRequest.cs | 2 +- .../NewtonsoftJsonSerializer.cs | 2 +- .../BaseSerializeNoCamelCaseTest.cs | 8 +++++ .../BaseSerializerTest.cs | 9 ++++++ .../TestData/SerializeToBytesTestData.cs | 32 +++++++++++++++++++ 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs index e5fa6999..c7796bb7 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs @@ -17,7 +17,7 @@ public GraphQLWebSocketRequest() public GraphQLWebSocketRequest(Abstractions.Websocket.GraphQLWebSocketRequest other) { Id = other.Id; Type = other.Type; - Payload = new Newtonsoft.GraphQLRequest(other.Payload); + Payload = new GraphQLRequest(other.Payload); // create serializer-specific type } } } diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index f7ec6269..055fab98 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -33,7 +33,7 @@ public string SerializeToString(GraphQL.GraphQLRequest request) { } public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest request) { - var json = JsonConvert.SerializeObject(request, Options.JsonSerializerSettings); + var json = JsonConvert.SerializeObject(new GraphQLWebSocketRequest(request), Options.JsonSerializerSettings); return Encoding.UTF8.GetBytes(json); } diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs index 833334b7..13106fcd 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs @@ -31,6 +31,14 @@ public void SerializeToStringTest(string expectedJson, GraphQLRequest request) { json.Should().Be(expectedJson.RemoveWhitespace()); } + [Theory] + [ClassData(typeof(SerializeToBytesTestData))] + public void SerializeToBytesTest(string expectedJson, GraphQLWebSocketRequest request) { + var json = Encoding.UTF8.GetString(Serializer.SerializeToBytes(request)).RemoveWhitespace(); + json.Should().Be(expectedJson.RemoveWhitespace()); + } + + [Fact] public async void WorksWithoutCamelCaseNamingStrategy() { diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs index 92226813..a07f38f1 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using System.Text; using FluentAssertions; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; @@ -32,6 +34,13 @@ public void SerializeToStringTest(string expectedJson, GraphQLRequest request) { json.Should().BeEquivalentTo(expectedJson.RemoveWhitespace()); } + [Theory] + [ClassData(typeof(SerializeToBytesTestData))] + public void SerializeToBytesTest(string expectedJson, GraphQLWebSocketRequest request) { + var json = Encoding.UTF8.GetString(Serializer.SerializeToBytes(request)).RemoveWhitespace(); + json.Should().BeEquivalentTo(expectedJson.RemoveWhitespace()); + } + [Fact] public async void CanDeserializeExtensions() { diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs new file mode 100644 index 00000000..41d15ce0 --- /dev/null +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs @@ -0,0 +1,32 @@ +using System.Collections; +using System.Collections.Generic; +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Tests.Common.Chat; + +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}}", + new GraphQLWebSocketRequest { + Id = "1234567", + Type = GraphQLWebSocketMessageType.GQL_START, + Payload = new GraphQLRequest("simplequerystring") + } + }; + yield return new object[] { + "{\"id\":\"34476567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"}}}", + new GraphQLWebSocketRequest { + Id = "34476567", + Type = GraphQLWebSocketMessageType.GQL_START, + Payload = new GraphQLRequest("simple query string", new { camelCaseProperty = "camelCase", PascalCaseProperty = "PascalCase"}) + } + + }; + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + } +} From a6f192b7fdb4227558ecd43feb316419e2db4377 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 12 Feb 2020 15:18:45 +0100 Subject: [PATCH 19/55] remove Options classes from serializers, fix GraphQLWebsocketRequest transformation --- .../GraphQLJsonSerializerExtensions.cs | 6 +- .../GraphQLWebSocketRequest.cs | 2 +- .../NewtonsoftJsonSerializer.cs | 27 +-- .../NewtonsoftJsonSerializerOptions.cs | 10 -- .../GraphQLWebSocketRequest.cs | 2 +- .../SystemTextJsonSerializer.cs | 27 +-- .../SystemTextJsonSerializerOptions.cs | 10 -- .../NewtonsoftSerializerTest.cs | 2 +- .../SystemTextJsonSerializerTests.cs | 2 +- .../GraphQL.Client.Tests.Common.csproj | 2 +- .../Helpers/CallbackTester.cs | 9 +- .../Helpers/ObservableTester.cs | 155 +++++++++--------- .../WebsocketTest.cs | 139 ++++++++-------- 13 files changed, 190 insertions(+), 203 deletions(-) delete mode 100644 src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs delete mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs diff --git a/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs b/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs index e544e75f..960f687f 100644 --- a/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs +++ b/src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs @@ -22,8 +22,10 @@ public static TSerializerInterface EnsureAssigned(this TSe return jsonSerializer; } - public static TOptions New(this Action configure) { - var options = Activator.CreateInstance(); + public static TOptions New(this Action configure) => + configure.AndReturn(Activator.CreateInstance()); + + public static TOptions AndReturn(this Action configure, TOptions options) { configure(options); return options; } diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs index c7796bb7..c575b9d3 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs @@ -17,7 +17,7 @@ public GraphQLWebSocketRequest() public GraphQLWebSocketRequest(Abstractions.Websocket.GraphQLWebSocketRequest other) { Id = other.Id; Type = other.Type; - Payload = new GraphQLRequest(other.Payload); // create serializer-specific 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 055fab98..a40ad456 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -1,39 +1,44 @@ using System; using System.IO; -using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace GraphQL.Client.Serializer.Newtonsoft { public class NewtonsoftJsonSerializer: IGraphQLWebsocketJsonSerializer { - public NewtonsoftJsonSerializerOptions Options { get; } + public static JsonSerializerSettings DefaultJsonSerializerSettings => new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver { IgnoreIsSpecifiedMembers = true }, + MissingMemberHandling = MissingMemberHandling.Ignore + }; - public NewtonsoftJsonSerializer() : this(new NewtonsoftJsonSerializerOptions()) { } + public JsonSerializerSettings JsonSerializerSettings { get; } - public NewtonsoftJsonSerializer(Action configure) : this(configure.New()) { } + public NewtonsoftJsonSerializer() : this(DefaultJsonSerializerSettings) { } - public NewtonsoftJsonSerializer(NewtonsoftJsonSerializerOptions options) { - Options = options; + public NewtonsoftJsonSerializer(Action configure) : this(configure.AndReturn(DefaultJsonSerializerSettings)) { } + + public NewtonsoftJsonSerializer(JsonSerializerSettings jsonSerializerSettings) { + JsonSerializerSettings = jsonSerializerSettings; ConfigureMandatorySerializerOptions(); } private void ConfigureMandatorySerializerOptions() { // deserialize extensions to Dictionary - Options.JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); + JsonSerializerSettings.Converters.Insert(0, new GraphQLExtensionsConverter()); } public string SerializeToString(GraphQL.GraphQLRequest request) { - return JsonConvert.SerializeObject(new GraphQLRequest(request), Options.JsonSerializerSettings); + return JsonConvert.SerializeObject(new GraphQLRequest(request), JsonSerializerSettings); } public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest request) { - var json = JsonConvert.SerializeObject(new GraphQLWebSocketRequest(request), Options.JsonSerializerSettings); + var json = JsonConvert.SerializeObject(new GraphQLWebSocketRequest(request), JsonSerializerSettings); return Encoding.UTF8.GetBytes(json); } @@ -43,7 +48,7 @@ public Task DeserializeToWebsocketResponseWrapperAsync public GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes) { return JsonConvert.DeserializeObject>>(Encoding.UTF8.GetString(bytes), - Options.JsonSerializerSettings); + JsonSerializerSettings); } public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { @@ -54,7 +59,7 @@ public Task> DeserializeFromUtf8StreamAsync DeserializeFromUtf8Stream(Stream stream) { using StreamReader sr = new StreamReader(stream); using JsonReader reader = new JsonTextReader(sr); - JsonSerializer serializer = JsonSerializer.Create(Options.JsonSerializerSettings); + JsonSerializer serializer = JsonSerializer.Create(JsonSerializerSettings); return Task.FromResult(serializer.Deserialize(reader)); } diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs deleted file mode 100644 index d05f1dda..00000000 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializerOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace GraphQL.Client.Serializer.Newtonsoft { - public class NewtonsoftJsonSerializerOptions { - public JsonSerializerSettings JsonSerializerSettings { get; set; } = new JsonSerializerSettings { - ContractResolver = new CamelCasePropertyNamesContractResolver { IgnoreIsSpecifiedMembers = true } - }; - } -} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs index f7599391..4961e00d 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs @@ -17,7 +17,7 @@ public GraphQLWebSocketRequest() public GraphQLWebSocketRequest(Abstractions.Websocket.GraphQLWebSocketRequest other) { Id = other.Id; Type = other.Type; - Payload = new GraphQLRequest(other.Payload); + Payload = other.Payload != null ? new GraphQLRequest(other.Payload) : null; // create serializer-specific type; } } } diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index 51f7b796..420a8522 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Dahomey.Json; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; @@ -10,43 +11,47 @@ namespace GraphQL.Client.Serializer.SystemTextJson { public class SystemTextJsonSerializer: IGraphQLWebsocketJsonSerializer { - public SystemTextJsonSerializerOptions Options { get; } + public static JsonSerializerOptions DefaultJsonSerializerOptions => new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }.SetupExtensions(); - public SystemTextJsonSerializer() : this(new SystemTextJsonSerializerOptions()) { } + public JsonSerializerOptions Options { get; } - public SystemTextJsonSerializer(Action configure) : this(configure.New()) { } + public SystemTextJsonSerializer() : this(DefaultJsonSerializerOptions) { } - public SystemTextJsonSerializer(SystemTextJsonSerializerOptions options) { + public SystemTextJsonSerializer(Action configure) : this(configure.AndReturn(DefaultJsonSerializerOptions)) { } + + public SystemTextJsonSerializer(JsonSerializerOptions options) { Options = options; ConfigureMandatorySerializerOptions(); } private void ConfigureMandatorySerializerOptions() { // deserialize extensions to Dictionary - Options.JsonSerializerOptions.Converters.Insert(0, new GraphQLExtensionsConverter()); + Options.Converters.Insert(0, new GraphQLExtensionsConverter()); // allow the JSON field "data" to match the property "Data" even without JsonNamingPolicy.CamelCase - Options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + Options.PropertyNameCaseInsensitive = true; } public string SerializeToString(GraphQL.GraphQLRequest request) { - return JsonSerializer.Serialize(new GraphQLRequest(request), Options.JsonSerializerOptions); + return JsonSerializer.Serialize(new GraphQLRequest(request), Options); } public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { - return JsonSerializer.DeserializeAsync>(stream, Options.JsonSerializerOptions, cancellationToken).AsTask(); + return JsonSerializer.DeserializeAsync>(stream, Options, cancellationToken).AsTask(); } public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest request) { - return JsonSerializer.SerializeToUtf8Bytes(new GraphQLWebSocketRequest(request), Options.JsonSerializerOptions); + return JsonSerializer.SerializeToUtf8Bytes(new GraphQLWebSocketRequest(request), Options); } public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { - return JsonSerializer.DeserializeAsync(stream, Options.JsonSerializerOptions).AsTask(); + return JsonSerializer.DeserializeAsync(stream, Options).AsTask(); } public GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes) { return JsonSerializer.Deserialize>>(new ReadOnlySpan(bytes), - Options.JsonSerializerOptions); + Options); } } diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs deleted file mode 100644 index af9bbc35..00000000 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializerOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json; -using Dahomey.Json; - -namespace GraphQL.Client.Serializer.SystemTextJson { - public class SystemTextJsonSerializerOptions { - public JsonSerializerOptions JsonSerializerOptions { get; set; } = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }.SetupExtensions(); - } -} diff --git a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs index 5e77e74d..84bb5e84 100644 --- a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs @@ -8,6 +8,6 @@ public NewtonsoftSerializerTest() : base(new NewtonsoftJsonSerializer()) { } public class NewtonsoftSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest { public NewtonsoftSerializeNoCamelCaseTest() - : base(new NewtonsoftJsonSerializer(options => options.JsonSerializerSettings = new JsonSerializerSettings())) { } + : base(new NewtonsoftJsonSerializer(new JsonSerializerSettings())) { } } } diff --git a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs index c0bec30f..a3d393dc 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(options => options.JsonSerializerOptions = new JsonSerializerOptions().SetupExtensions())) { } + : base(new SystemTextJsonSerializer(new JsonSerializerOptions().SetupExtensions())) { } } } diff --git a/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj index 3919581f..8526c03a 100644 --- a/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj +++ b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj @@ -5,13 +5,13 @@ + - diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs index 7525d2b5..c8ca29c5 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs @@ -1,6 +1,6 @@ using System; using System.Threading; -using Xunit; +using FluentAssertions; namespace GraphQL.Client.Tests.Common.Helpers { public class CallbackTester { @@ -32,8 +32,8 @@ public void Callback(T param) { /// action to assert the contents of the payload public void CallbackShouldHaveBeenInvoked(Action assertPayload = null, TimeSpan? timeout = null) { try { - if (!_callbackInvoked.Wait(timeout ?? Timeout)) - Assert.True(false, $"callback not invoked within {(timeout ?? Timeout).TotalSeconds} s!"); + _callbackInvoked.Wait(timeout ?? Timeout).Should().BeTrue("because the callback method should have been invoked (timeout: {0} s)", + (timeout ?? Timeout).TotalSeconds); assertPayload?.Invoke(LastPayload); } @@ -49,8 +49,7 @@ public void CallbackShouldHaveBeenInvoked(Action assertPayload = null, TimeSp public void CallbackShouldNotHaveBeenInvoked(TimeSpan? timeout = null) { if (!timeout.HasValue) timeout = TimeSpan.FromMilliseconds(100); try { - if (_callbackInvoked.Wait(timeout.Value)) - Assert.True(false, "callback was inadvertently invoked pushed!"); + _callbackInvoked.Wait(timeout.Value).Should().BeFalse("because the callback method should not have been invoked"); } finally { Reset(); diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 55c63c51..1b8d8030 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -1,27 +1,31 @@ using System; +using System.Reactive.Concurrency; +using System.Reactive.Linq; using System.Threading; -using Xunit; +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Primitives; namespace GraphQL.Client.Tests.Common.Helpers { - public class ObservableTester : IDisposable { - private readonly IDisposable _subscription; - private ManualResetEventSlim _updateReceived { get; } = new ManualResetEventSlim(); - private ManualResetEventSlim _completed { get; } = new ManualResetEventSlim(); - private ManualResetEventSlim _error { get; } = new ManualResetEventSlim(); + public class ObservableTester : IDisposable { + private readonly IDisposable subscription; + private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); + private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); + private readonly ManualResetEventSlim error = new ManualResetEventSlim(); /// /// The timeout for . Defaults to 1 s /// - public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(3); + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1); /// /// Indicates that an update has been received since the last /// - public bool UpdateReceived => _updateReceived.IsSet; + public bool UpdateReceived => updateReceived.IsSet; /// /// The last payload which was received. /// - public T LastPayload { get; private set; } + public TPayload LastPayload { get; private set; } public Exception Error { get; private set; } @@ -29,101 +33,104 @@ public class ObservableTester : IDisposable { /// Creates a new which subscribes to the supplied /// /// the under test - public ObservableTester(IObservable observable) { - _subscription = observable.Subscribe( + public ObservableTester(IObservable observable) { + subscription = observable.ObserveOn(TaskPoolScheduler.Default).Subscribe( obj => { LastPayload = obj; - _updateReceived.Set(); + updateReceived.Set(); }, ex => { Error = ex; - _error.Set(); + error.Set(); }, - () => _completed.Set() + () => completed.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. + /// Resets the tester class. Should be called before triggering the potential update /// - /// action to assert the contents of the payload - public void ShouldHaveReceivedUpdate(Action assertPayload = null, TimeSpan? timeout = null) { - try { - if (!_updateReceived.Wait(timeout ?? Timeout)) - Assert.True(false, $"no update received within {(timeout ?? Timeout).TotalSeconds} s!"); + private void Reset() { + updateReceived.Reset(); + } - assertPayload?.Invoke(LastPayload); - } - finally { - _reset(); - } + /// + public void Dispose() { + subscription?.Dispose(); } - /// - /// 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 ShouldNotHaveReceivedUpdate(TimeSpan? timeout = null) { - if (!timeout.HasValue) timeout = TimeSpan.FromMilliseconds(100); - try { - if (_updateReceived.Wait(timeout.Value)) - Assert.True(false, "update was inadvertently pushed!"); - } - finally { - _reset(); - } + public SubscriptionAssertions Should() { + return new SubscriptionAssertions(this); } - /// - /// Asserts that the subscription has completed within the configured since the last - /// - public void ShouldHaveCompleted(TimeSpan? timeout = null) { - try { - if (!_completed.Wait(timeout ?? Timeout)) - Assert.True(false, $"subscription did not complete within {(timeout ?? Timeout).TotalSeconds} s!"); + public class SubscriptionAssertions : ReferenceTypeAssertions, SubscriptionAssertions> { + public SubscriptionAssertions(ObservableTester tester) { + Subject = tester; } - finally { - _reset(); + + protected override string Identifier => "Subscription"; + + public AndWhichConstraint, TPayload> HaveReceivedPayload(TimeSpan timeout, + string because = "", params object[] becauseArgs) { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject.updateReceived.Wait(timeout)) + .ForCondition(isSet => isSet) + .FailWith("Expected {context:Subscription} to receive new payload{reason}, but did not receive an update within {0}", timeout); + + Subject.updateReceived.Reset(); + return new AndWhichConstraint, TPayload>(this, Subject.LastPayload); } - } + public AndWhichConstraint, TPayload> HaveReceivedPayload(string because = "", params object[] becauseArgs) + => HaveReceivedPayload(Subject.Timeout, because, becauseArgs); - /// - /// Asserts that the subscription has completed within the configured since the last - /// - public void ShouldHaveThrownError(Action assertError = null, TimeSpan? timeout = null) { - try { - if (!_error.Wait(timeout ?? Timeout)) - Assert.True(false, $"subscription did not throw an error within {(timeout ?? Timeout).TotalSeconds} s!"); + public AndConstraint> NotHaveReceivedPayload(TimeSpan timeout, + string because = "", params object[] becauseArgs) { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject.updateReceived.Wait(timeout)) + .ForCondition(isSet => !isSet) + .FailWith("Expected {context:Subscription} to not receive a new payload{reason}, but did receive an update: {0}", Subject.LastPayload); - assertError?.Invoke(Error); + Subject.updateReceived.Reset(); + return new AndConstraint>(this); } - finally { - _reset(); + public AndConstraint> NotHaveReceivedPayload(string because = "", params object[] becauseArgs) + => NotHaveReceivedPayload(TimeSpan.FromMilliseconds(100), because, becauseArgs); + + public AndWhichConstraint, Exception> HaveReceivedError(TimeSpan timeout, + string because = "", params object[] becauseArgs) { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject.error.Wait(timeout)) + .ForCondition(isSet => isSet) + .FailWith("Expected {context:Subscription} to fail{reason}, but did not receive an error within {0}", timeout); + + return new AndWhichConstraint, Exception>(this, Subject.Error); } - } + public AndWhichConstraint, Exception> HaveReceivedError(string because = "", params object[] becauseArgs) + => HaveReceivedError(Subject.Timeout, because, becauseArgs); - /// - /// Resets the tester class. Should be called before triggering the potential update - /// - private void _reset() { - //if (_completed.IsSet) - // throw new InvalidOperationException( - // "the subscription sequence has completed. this tester instance cannot be reused"); - LastPayload = default(T); - _updateReceived.Reset(); - } + public AndConstraint> HaveCompleted(TimeSpan timeout, + string because = "", params object[] becauseArgs) { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject.completed.Wait(timeout)) + .ForCondition(isSet => isSet) + .FailWith("Expected {context:Subscription} to complete{reason}, but did not complete within {0}", timeout); - /// - public void Dispose() { - _subscription?.Dispose(); + return new AndConstraint>(this); + } + public AndConstraint> HaveCompleted(string because = "", params object[] becauseArgs) + => HaveCompleted(Subject.Timeout, because, becauseArgs); } } public static class ObservableExtensions { - public static ObservableTester SubscribeTester(this IObservable observable) { + public static ObservableTester Monitor(this IObservable observable) { return new ObservableTester(observable); } } + } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTest.cs b/tests/GraphQL.Integration.Tests/WebsocketTest.cs index baf7569c..a568693e 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTest.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTest.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Hosting; using Xunit; using Xunit.Abstractions; +using FluentAssertions; namespace GraphQL.Integration.Tests { public class WebsocketTest { @@ -78,26 +79,24 @@ public async void CanCreateObservableSubscription() { IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest); Debug.WriteLine("subscribing..."); - var tester = observable.SubscribeTester(); - const string message1 = "Hello World"; - - var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - Assert.Equal(message1, response.Data.AddMessage.Content); - - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal(message1, gqlResponse.Data.MessageAdded.Content); - }); - - const string message2 = "lorem ipsum dolor si amet"; - response = await client.AddMessageAsync(message2).ConfigureAwait(false); - Assert.Equal(message2, response.Data.AddMessage.Content); - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal(message2, gqlResponse.Data.MessageAdded.Content); - }); - - // disposing the client should throw a TaskCanceledException on the subscription - client.Dispose(); - tester.ShouldHaveCompleted(); + 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(); + } } } @@ -120,21 +119,19 @@ public async void CanReconnectWithSameObservable() { IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest); Debug.WriteLine("subscribing..."); - var tester = observable.SubscribeTester(); + var tester = observable.Monitor(); const string message1 = "Hello World"; var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - Assert.Equal(message1, response.Data.AddMessage.Content); - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal(message1, gqlResponse.Data.MessageAdded.Content); - }); + 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); - Assert.Equal(message2, response.Data.AddMessage.Content); - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal(message2, gqlResponse.Data.MessageAdded.Content); - }); + response.Data.AddMessage.Content.Should().Be(message2); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message2); Debug.WriteLine("disposing subscription..."); tester.Dispose(); @@ -142,20 +139,19 @@ public async void CanReconnectWithSameObservable() { await client.InitializeWebsocketConnection(); Debug.WriteLine("creating new subscription..."); - tester = observable.SubscribeTester(); - tester.ShouldHaveReceivedUpdate( - gqlResponse => { Assert.Equal(message2, gqlResponse.Data.MessageAdded.Content); }, - TimeSpan.FromSeconds(10)); + 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); - Assert.Equal(message3, response.Data.AddMessage.Content); - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal(message3, gqlResponse.Data.MessageAdded.Content); - }); + 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.ShouldHaveCompleted(); + tester.Should().HaveCompleted(); } } @@ -193,39 +189,36 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { IObservable> observable2 = client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Callback); Debug.WriteLine("subscribing..."); - var tester = observable1.SubscribeTester(); - var tester2 = observable2.SubscribeTester(); + var tester = observable1.Monitor(); + var tester2 = observable2.Monitor(); const string message1 = "Hello World"; var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - Assert.Equal(message1, response.Data.AddMessage.Content); - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal(message1, gqlResponse.Data.MessageAdded.Content); - }); + response.Data.AddMessage.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message1); await Task.Delay(500); // ToDo: can be removed after https://github.com/graphql-dotnet/server/pull/199 was merged and released var joinResponse = await client.JoinDeveloperUser().ConfigureAwait(false); - Assert.Equal("developer", joinResponse.Data.Join.DisplayName); + joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); - tester2.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal("1", gqlResponse.Data.UserJoined.Id); - Assert.Equal("developer", gqlResponse.Data.UserJoined.DisplayName); - }); + 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); - Assert.Equal(message3, response.Data.AddMessage.Content); - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal(message3, gqlResponse.Data.MessageAdded.Content); - }); + 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.ShouldHaveCompleted(); + tester.Should().HaveCompleted(); } } @@ -241,15 +234,13 @@ public async void CanHandleConnectionTimeout() { IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Callback); Debug.WriteLine("subscribing..."); - var tester = observable.SubscribeTester(); + var tester = observable.Monitor(); const string message1 = "Hello World"; var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - Assert.Equal(message1, response.Data.AddMessage.Content); - - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Equal(message1, gqlResponse.Data.MessageAdded.Content); - }); + 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); @@ -268,7 +259,7 @@ public async void CanHandleConnectionTimeout() { // disposing the client should complete the subscription client.Dispose(); - tester.ShouldHaveCompleted(TimeSpan.FromSeconds(5)); + tester.Should().HaveCompleted(TimeSpan.FromSeconds(5)); server.Dispose(); } @@ -289,13 +280,12 @@ public async void CanHandleSubscriptionError() { ); Debug.WriteLine("subscribing..."); - var tester = observable.SubscribeTester(); - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Single(gqlResponse.Errors); - }); - tester.ShouldHaveCompleted(); - - client.Dispose(); + using (var tester = observable.Monitor()) { + tester.Should().HaveReceivedPayload() + .Which.Errors.Should().ContainSingle(); + tester.Should().HaveCompleted(); + client.Dispose(); + } } } @@ -319,13 +309,12 @@ public async void CanHandleQueryErrorInSubscription() { ); Debug.WriteLine("subscribing..."); - var tester = observable.SubscribeTester(); - tester.ShouldHaveReceivedUpdate(gqlResponse => { - Assert.Single(gqlResponse.Errors); - }); - tester.ShouldHaveCompleted(); - - client.Dispose(); + using (var tester = observable.Monitor()) { + tester.Should().HaveReceivedPayload() + .Which.Errors.Should().ContainSingle(); + tester.Should().HaveCompleted(); + client.Dispose(); + } } } } From a47cae1f0d483ae0cd514f05ccdc1831a64e02b5 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 12 Feb 2020 15:39:48 +0100 Subject: [PATCH 20/55] run websocket tests for all available serializers --- .../Helpers/AvailableJsonSerializers.cs | 24 ++++++ .../GraphQL.Integration.Tests.csproj | 2 + .../Helpers/WebHostHelpers.cs | 10 ++- .../NewtonsoftWebsocketTest.cs | 5 ++ .../WebsocketTest.cs | 83 +++++++++++-------- 5 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 tests/GraphQL.Client.Tests.Common/Helpers/AvailableJsonSerializers.cs create mode 100644 tests/GraphQL.Integration.Tests/NewtonsoftWebsocketTest.cs diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/AvailableJsonSerializers.cs b/tests/GraphQL.Client.Tests.Common/Helpers/AvailableJsonSerializers.cs new file mode 100644 index 00000000..637c5861 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Helpers/AvailableJsonSerializers.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Client.Abstractions; + +namespace GraphQL.Client.Tests.Common.Helpers { + public class AvailableJsonSerializers : IEnumerable where TSerializerInterface : IGraphQLJsonSerializer { + public IEnumerator GetEnumerator() { + // try to find one in the assembly and assign that + var type = typeof(TSerializerInterface); + return AppDomain.CurrentDomain + .GetAssemblies() + .SelectMany(s => s.GetTypes()) + .Where(p => type.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract) + .Select(serializerType => new object[]{Activator.CreateInstance(serializerType)}) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + } +} diff --git a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj index b1731cc1..3273adae 100644 --- a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj +++ b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj @@ -21,6 +21,8 @@ + + diff --git a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs index 7bb87cde..0ee54a08 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs @@ -1,5 +1,6 @@ using System; using GraphQL.Client; +using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; using GraphQL.Client.Serializer.Newtonsoft; using GraphQL.Client.Tests.Common.Helpers; @@ -33,19 +34,20 @@ public static IWebHost CreateServer(int port) where TStartup : class } - public static GraphQLHttpClient GetGraphQLClient(int port, bool requestsViaWebsocket = false) + public static GraphQLHttpClient GetGraphQLClient(int port, bool requestsViaWebsocket = false, IGraphQLWebsocketJsonSerializer serializer = null) => new GraphQLHttpClient(new GraphQLHttpClientOptions { EndPoint = new Uri($"http://localhost:{port}/graphql"), UseWebSocketForQueriesAndMutations = requestsViaWebsocket, - JsonSerializer = new NewtonsoftJsonSerializer() + JsonSerializer = serializer ?? new NewtonsoftJsonSerializer() }); - public static TestServerSetup SetupTest(bool requestsViaWebsocket = false) where TStartup : class + 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) + Client = GetGraphQLClient(port, requestsViaWebsocket, serializer) }; } } diff --git a/tests/GraphQL.Integration.Tests/NewtonsoftWebsocketTest.cs b/tests/GraphQL.Integration.Tests/NewtonsoftWebsocketTest.cs new file mode 100644 index 00000000..fd0491b0 --- /dev/null +++ b/tests/GraphQL.Integration.Tests/NewtonsoftWebsocketTest.cs @@ -0,0 +1,5 @@ +namespace GraphQL.Integration.Tests { + public class NewtonsoftWebsocketTest { + + } +} diff --git a/tests/GraphQL.Integration.Tests/WebsocketTest.cs b/tests/GraphQL.Integration.Tests/WebsocketTest.cs index a568693e..1715cd3c 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTest.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTest.cs @@ -12,22 +12,23 @@ using Xunit; using Xunit.Abstractions; using FluentAssertions; +using GraphQL.Client.Abstractions.Websocket; namespace GraphQL.Integration.Tests { public class WebsocketTest { private readonly ITestOutputHelper output; - private static IWebHost CreateServer(int port) => WebHostHelpers.CreateServer(port); public WebsocketTest(ITestOutputHelper output) { this.output = output; } - [Fact] - public async void AssertTestingHarness() { + [Theory] + [ClassData(typeof(AvailableJsonSerializers))] + public async void AssertTestingHarness(IGraphQLWebsocketJsonSerializer serializer) { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port); + var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); const string message = "some random testing message"; var response = await client.AddMessageAsync(message).ConfigureAwait(false); @@ -36,11 +37,13 @@ public async void AssertTestingHarness() { } } - [Fact] - public async void CanSendRequestViaWebsocket() { + + [Theory] + [ClassData(typeof(AvailableJsonSerializers))] + public async void CanSendRequestViaWebsocket(IGraphQLWebsocketJsonSerializer serializer) { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, true); + var client = WebHostHelpers.GetGraphQLClient(port, true, serializer); const string message = "some random testing message"; var response = await client.AddMessageAsync(message).ConfigureAwait(false); @@ -48,11 +51,13 @@ public async void CanSendRequestViaWebsocket() { } } - [Fact] - public async void CanHandleRequestErrorViaWebsocket() { + + [Theory] + [ClassData(typeof(AvailableJsonSerializers))] + public async void CanHandleRequestErrorViaWebsocket(IGraphQLWebsocketJsonSerializer serializer) { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, true); + var client = WebHostHelpers.GetGraphQLClient(port, true, serializer); var response = await client.SendQueryAsync("this query is formatted quite badly").ConfigureAwait(false); Assert.Single(response.Errors); @@ -68,11 +73,13 @@ public async void CanHandleRequestErrorViaWebsocket() { private readonly GraphQLRequest SubscriptionRequest = new GraphQLRequest(SubscriptionQuery); - [Fact] - public async void CanCreateObservableSubscription() { + + [Theory] + [ClassData(typeof(AvailableJsonSerializers))] + public async void CanCreateObservableSubscription(IGraphQLWebsocketJsonSerializer serializer) { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port); + var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); await client.InitializeWebsocketConnection(); Debug.WriteLine("creating subscription stream"); @@ -108,11 +115,13 @@ public class MessageAddedContent { } } - [Fact] - public async void CanReconnectWithSameObservable() { + + [Theory] + [ClassData(typeof(AvailableJsonSerializers))] + public async void CanReconnectWithSameObservable(IGraphQLWebsocketJsonSerializer serializer) { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port); + var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); await client.InitializeWebsocketConnection(); Debug.WriteLine("creating subscription stream"); @@ -175,18 +184,22 @@ public class UserJoinedContent { private readonly GraphQLRequest SubscriptionRequest2 = new GraphQLRequest(SubscriptionQuery2); - [Fact] - public async void CanConnectTwoSubscriptionsSimultaneously() { + + [Theory] + [ClassData(typeof(AvailableJsonSerializers))] + public async void CanConnectTwoSubscriptionsSimultaneously(IGraphQLWebsocketJsonSerializer serializer) { var port = NetworkHelpers.GetFreeTcpPortNumber(); var callbackTester = new CallbackTester(); var callbackTester2 = new CallbackTester(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port); + var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); await client.InitializeWebsocketConnection(); Debug.WriteLine("creating subscription stream"); - IObservable> observable1 = client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Callback); - IObservable> observable2 = client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Callback); + IObservable> observable1 = + client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Callback); + IObservable> observable2 = + client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Callback); Debug.WriteLine("subscribing..."); var tester = observable1.Monitor(); @@ -197,9 +210,7 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { response.Data.AddMessage.Content.Should().Be(message1); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message1); - - await Task.Delay(500); // ToDo: can be removed after https://github.com/graphql-dotnet/server/pull/199 was merged and released - + var joinResponse = await client.JoinDeveloperUser().ConfigureAwait(false); joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); @@ -222,13 +233,15 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { } } - [Fact] - public async void CanHandleConnectionTimeout() { + + [Theory] + [ClassData(typeof(AvailableJsonSerializers))] + public async void CanHandleConnectionTimeout(IGraphQLWebsocketJsonSerializer serializer) { var port = NetworkHelpers.GetFreeTcpPortNumber(); var server = CreateServer(port); var callbackTester = new CallbackTester(); - var client = WebHostHelpers.GetGraphQLClient(port); + var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); await client.InitializeWebsocketConnection(); Debug.WriteLine("creating subscription stream"); IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Callback); @@ -263,11 +276,13 @@ public async void CanHandleConnectionTimeout() { server.Dispose(); } - [Fact] - public async void CanHandleSubscriptionError() { + + [Theory] + [ClassData(typeof(AvailableJsonSerializers))] + public async void CanHandleSubscriptionError(IGraphQLWebsocketJsonSerializer serializer) { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port); + var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); await client.InitializeWebsocketConnection(); Debug.WriteLine("creating subscription stream"); IObservable> observable = client.CreateSubscriptionStream( @@ -289,14 +304,16 @@ public async void CanHandleSubscriptionError() { } } - [Fact] - public async void CanHandleQueryErrorInSubscription() { + + [Theory] + [ClassData(typeof(AvailableJsonSerializers))] + public async void CanHandleQueryErrorInSubscription(IGraphQLWebsocketJsonSerializer serializer) { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { var test = new GraphQLRequest("tset", new { test = "blaa" }); - var client = WebHostHelpers.GetGraphQLClient(port); + var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); await client.InitializeWebsocketConnection(); Debug.WriteLine("creating subscription stream"); IObservable> observable = client.CreateSubscriptionStream( From 673ccaa4c9a024226e74077ac7182bea5974f5f9 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Thu, 13 Feb 2020 08:21:34 +0100 Subject: [PATCH 21/55] Fix example in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f55b48dd..3f0c61ff 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ var heroAndFriendsRequest = new GraphQLRequest { ### Execute Query/Mutation: ```csharp -var graphQLClient = new GraphQLClient("https://swapi.apis.guru/"); +var graphQLClient = new GraphQLHttpClient("https://swapi.apis.guru/"); public class HeroAndFriendsResponse { public Hero Hero {get; set;} From 94c2d918c3a650fa089a80dabf56e12faf419d6e Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Fri, 14 Feb 2020 11:25:31 +0100 Subject: [PATCH 22/55] restructure test classes --- .../ExtensionsTest.cs | 34 ---------- .../NewtonsoftWebsocketTest.cs | 5 -- .../Base.cs} | 18 +++-- .../QueryAndMutationTests/Newtonsoft.cs | 9 +++ .../QueryAndMutationTests/SystemTextJson.cs | 9 +++ .../Base.cs} | 66 +++++++++---------- .../WebsocketTests/Newtonsoft.cs | 10 +++ .../WebsocketTests/SystemTextJson.cs | 10 +++ 8 files changed, 81 insertions(+), 80 deletions(-) delete mode 100644 tests/GraphQL.Integration.Tests/ExtensionsTest.cs delete mode 100644 tests/GraphQL.Integration.Tests/NewtonsoftWebsocketTest.cs rename tests/GraphQL.Integration.Tests/{QueryAndMutationTests.cs => QueryAndMutationTests/Base.cs} (90%) create mode 100644 tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs create mode 100644 tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs rename tests/GraphQL.Integration.Tests/{WebsocketTest.cs => WebsocketTests/Base.cs} (84%) create mode 100644 tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs create mode 100644 tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs diff --git a/tests/GraphQL.Integration.Tests/ExtensionsTest.cs b/tests/GraphQL.Integration.Tests/ExtensionsTest.cs deleted file mode 100644 index cdfb5622..00000000 --- a/tests/GraphQL.Integration.Tests/ExtensionsTest.cs +++ /dev/null @@ -1,34 +0,0 @@ -using GraphQL.Integration.Tests.Helpers; -using IntegrationTestServer; - -namespace GraphQL.Integration.Tests { - public class ExtensionsTest { - private static TestServerSetup SetupTest(bool requestsViaWebsocket = false) => - WebHostHelpers.SetupTest(requestsViaWebsocket); - - //[Fact] - //public async void DontNeedToUseCamelCaseNamingStrategy() { - - // using var setup = SetupTest(); - // setup.Client.Options.JsonSerializerSettings = new JsonSerializerSettings(); - - // const string message = "some random testing message"; - // var graphQLRequest = new GraphQLRequest( - // @"mutation($input: MessageInputType){ - // addMessage(message: $input){ - // content - // } - // }", - // new { - // input = new { - // fromId = "2", - // content = message, - // sentAt = DateTime.Now - // } - // }); - // var response = await setup.Client.SendMutationAsync(graphQLRequest, () => new { addMessage = new { content = "" } }); - - // Assert.Equal(message, response.Data.addMessage.content); - //} - } -} diff --git a/tests/GraphQL.Integration.Tests/NewtonsoftWebsocketTest.cs b/tests/GraphQL.Integration.Tests/NewtonsoftWebsocketTest.cs deleted file mode 100644 index fd0491b0..00000000 --- a/tests/GraphQL.Integration.Tests/NewtonsoftWebsocketTest.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace GraphQL.Integration.Tests { - public class NewtonsoftWebsocketTest { - - } -} diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs similarity index 90% rename from tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs rename to tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index fa6e301e..be153ba2 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -1,19 +1,25 @@ using System.Net.Http; -using System.Text.Json; using GraphQL.Client.Abstractions; +using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Client.Tests.Common.StarWars; using GraphQL.Integration.Tests.Helpers; using IntegrationTestServer; -using Newtonsoft.Json.Linq; using Xunit; -namespace GraphQL.Integration.Tests { - public class QueryAndMutationTests { +namespace GraphQL.Integration.Tests.QueryAndMutationTests { + + public abstract class Base { + + protected IGraphQLWebsocketJsonSerializer serializer; + + private TestServerSetup SetupTest(bool requestsViaWebsocket = false) => WebHostHelpers.SetupTest(requestsViaWebsocket, serializer); + + protected Base(IGraphQLWebsocketJsonSerializer serializer) { + this.serializer = serializer; + } - private static TestServerSetup SetupTest(bool requestsViaWebsocket = false) => WebHostHelpers.SetupTest(requestsViaWebsocket); - [Theory] [ClassData(typeof(StarWarsHumans))] public async void QueryTheory(int id, string name) { diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs new file mode 100644 index 00000000..1046ed6d --- /dev/null +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs @@ -0,0 +1,9 @@ +using GraphQL.Client.Serializer.Newtonsoft; + +namespace GraphQL.Integration.Tests.QueryAndMutationTests { + public class Newtonsoft: Base { + public Newtonsoft() : base(new NewtonsoftJsonSerializer()) + { + } + } +} diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs new file mode 100644 index 00000000..dd725b4d --- /dev/null +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs @@ -0,0 +1,9 @@ +using GraphQL.Client.Serializer.SystemTextJson; + +namespace GraphQL.Integration.Tests.QueryAndMutationTests { + public class SystemTextJson: Base { + public SystemTextJson() : base(new SystemTextJsonSerializer()) + { + } + } +} diff --git a/tests/GraphQL.Integration.Tests/WebsocketTest.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs similarity index 84% rename from tests/GraphQL.Integration.Tests/WebsocketTest.cs rename to tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 1715cd3c..ff1cabe1 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTest.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -3,7 +3,9 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using FluentAssertions; using GraphQL.Client.Abstractions; +using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Tests.Common.Chat; using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; @@ -11,21 +13,24 @@ using Microsoft.AspNetCore.Hosting; using Xunit; using Xunit.Abstractions; -using FluentAssertions; -using GraphQL.Client.Abstractions.Websocket; -namespace GraphQL.Integration.Tests { - public class WebsocketTest { - private readonly ITestOutputHelper output; - private static IWebHost CreateServer(int port) => WebHostHelpers.CreateServer(port); +namespace GraphQL.Integration.Tests.WebsocketTests { + public abstract class Base { + protected readonly ITestOutputHelper output; + protected readonly IGraphQLWebsocketJsonSerializer serializer; + protected IWebHost CreateServer(int port) => WebHostHelpers.CreateServer(port); - public WebsocketTest(ITestOutputHelper output) { + public Base(ITestOutputHelper output, IGraphQLWebsocketJsonSerializer serializer) { + this.output = output; + this.serializer = serializer; + } + + public Base(ITestOutputHelper output) { this.output = output; } - [Theory] - [ClassData(typeof(AvailableJsonSerializers))] - public async void AssertTestingHarness(IGraphQLWebsocketJsonSerializer serializer) { + [Fact] + public async void AssertTestingHarness() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); @@ -38,9 +43,8 @@ public async void AssertTestingHarness(IGraphQLWebsocketJsonSerializer serialize } - [Theory] - [ClassData(typeof(AvailableJsonSerializers))] - public async void CanSendRequestViaWebsocket(IGraphQLWebsocketJsonSerializer serializer) { + [Fact] + public async void CanSendRequestViaWebsocket() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { var client = WebHostHelpers.GetGraphQLClient(port, true, serializer); @@ -51,10 +55,8 @@ public async void CanSendRequestViaWebsocket(IGraphQLWebsocketJsonSerializer ser } } - - [Theory] - [ClassData(typeof(AvailableJsonSerializers))] - public async void CanHandleRequestErrorViaWebsocket(IGraphQLWebsocketJsonSerializer serializer) { + [Fact] + public async void CanHandleRequestErrorViaWebsocket() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { var client = WebHostHelpers.GetGraphQLClient(port, true, serializer); @@ -74,9 +76,8 @@ public async void CanHandleRequestErrorViaWebsocket(IGraphQLWebsocketJsonSeriali private readonly GraphQLRequest SubscriptionRequest = new GraphQLRequest(SubscriptionQuery); - [Theory] - [ClassData(typeof(AvailableJsonSerializers))] - public async void CanCreateObservableSubscription(IGraphQLWebsocketJsonSerializer serializer) { + [Fact] + public async void CanCreateObservableSubscription() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); @@ -116,9 +117,8 @@ public class MessageAddedContent { } - [Theory] - [ClassData(typeof(AvailableJsonSerializers))] - public async void CanReconnectWithSameObservable(IGraphQLWebsocketJsonSerializer serializer) { + [Fact] + public async void CanReconnectWithSameObservable() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); @@ -185,9 +185,8 @@ public class UserJoinedContent { private readonly GraphQLRequest SubscriptionRequest2 = new GraphQLRequest(SubscriptionQuery2); - [Theory] - [ClassData(typeof(AvailableJsonSerializers))] - public async void CanConnectTwoSubscriptionsSimultaneously(IGraphQLWebsocketJsonSerializer serializer) { + [Fact] + public async void CanConnectTwoSubscriptionsSimultaneously() { var port = NetworkHelpers.GetFreeTcpPortNumber(); var callbackTester = new CallbackTester(); var callbackTester2 = new CallbackTester(); @@ -234,9 +233,8 @@ public async void CanConnectTwoSubscriptionsSimultaneously(IGraphQLWebsocketJson } - [Theory] - [ClassData(typeof(AvailableJsonSerializers))] - public async void CanHandleConnectionTimeout(IGraphQLWebsocketJsonSerializer serializer) { + [Fact] + public async void CanHandleConnectionTimeout() { var port = NetworkHelpers.GetFreeTcpPortNumber(); var server = CreateServer(port); var callbackTester = new CallbackTester(); @@ -277,9 +275,8 @@ public async void CanHandleConnectionTimeout(IGraphQLWebsocketJsonSerializer ser } - [Theory] - [ClassData(typeof(AvailableJsonSerializers))] - public async void CanHandleSubscriptionError(IGraphQLWebsocketJsonSerializer serializer) { + [Fact] + public async void CanHandleSubscriptionError() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); @@ -305,9 +302,8 @@ public async void CanHandleSubscriptionError(IGraphQLWebsocketJsonSerializer ser } - [Theory] - [ClassData(typeof(AvailableJsonSerializers))] - public async void CanHandleQueryErrorInSubscription(IGraphQLWebsocketJsonSerializer serializer) { + [Fact] + public async void CanHandleQueryErrorInSubscription() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs new file mode 100644 index 00000000..02a0b030 --- /dev/null +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs @@ -0,0 +1,10 @@ +using GraphQL.Client.Serializer.Newtonsoft; +using Xunit.Abstractions; + +namespace GraphQL.Integration.Tests.WebsocketTests { + public class Newtonsoft: Base { + public Newtonsoft(ITestOutputHelper output) : base(output, new NewtonsoftJsonSerializer()) + { + } + } +} diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs new file mode 100644 index 00000000..3a7882e4 --- /dev/null +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs @@ -0,0 +1,10 @@ +using GraphQL.Client.Serializer.SystemTextJson; +using Xunit.Abstractions; + +namespace GraphQL.Integration.Tests.WebsocketTests { + public class SystemTextJson: Base { + public SystemTextJson(ITestOutputHelper output) : base(output, new SystemTextJsonSerializer()) + { + } + } +} From 1a35989a357360a43946dbf3e31891cfc6724993 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Sun, 16 Feb 2020 12:48:52 +0100 Subject: [PATCH 23/55] Update README.md Added note on using `byte[]` in variables (see #163) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3f0c61ff..95d46939 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ var heroAndFriendsRequest = new GraphQLRequest { }; ``` +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/"); @@ -62,6 +64,8 @@ var graphQLResponse = await graphQLClient.SendQueryAsync var heroName = graphQLResponse.Data.Hero.Name; ``` + + ### Use Subscriptions ```csharp From bfa906ba25f4505200d6564ac44f6091c327561e Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 17 Feb 2020 10:14:49 +0100 Subject: [PATCH 24/55] fix dispose on websocket --- src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 579be9e7..a79eb597 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -21,7 +21,7 @@ internal class GraphQLHttpWebSocket : IDisposable { private Subject _responseSubject; private readonly Subject _requestSubject = new Subject(); private readonly Subject _exceptionSubject = new Subject(); - private IDisposable _requestSubscription; + private readonly IDisposable _requestSubscription; public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None; @@ -272,6 +272,7 @@ private async Task DisposeAsync() { if (!_cancellationTokenSource.IsCancellationRequested) _cancellationTokenSource.Cancel(); await _closeAsync().ConfigureAwait(false); + _requestSubscription?.Dispose(); clientWebSocket?.Dispose(); _cancellationTokenSource.Dispose(); Debug.WriteLine($"websocket {clientWebSocket.GetHashCode()} disposed"); From d3fa68675400501c10b6fe1299a26984cf9221d3 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 17 Feb 2020 10:35:01 +0100 Subject: [PATCH 25/55] refactor disposing to better match steven clearys example --- .../Websocket/GraphQLHttpWebSocket.cs | 28 ++++++++++++------- .../GraphQL.Client.Serializer.Tests.csproj | 3 +- .../GraphQL.Client.Tests.Common.csproj | 1 + 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index a79eb597..2b2f1db4 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -90,7 +90,7 @@ private Task _backOff() { public Task InitializeWebSocket() { // do not attempt to initialize if cancellation is requested - if (_disposed != null) + if (Completion != null) throw new OperationCanceledException(); lock (_initializeLock) { @@ -254,20 +254,28 @@ private async Task _closeAsync(CancellationToken cancellationToken = default) { await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cancellationToken).ConfigureAwait(false); } -#endregion + #endregion -#region IDisposable + #region IDisposable + public void Dispose() => Complete(); - private Task _disposed; - private object _disposedLocker = new object(); - public void Dispose() { - // Async disposal as recommended by Stephen Cleary (https://blog.stephencleary.com/2013/03/async-oop-6-disposal.html) - lock (_disposedLocker) { - if (_disposed == null) _disposed = DisposeAsync(); + /// + /// Cancels the current operation, closes the websocket connection and disposes of internal resources. + /// + public void Complete() { + lock (completedLocker) { + if (Completion == null) Completion = CompleteAsync(); } } - private async Task DisposeAsync() { + /// + /// Task to await the completion (a.k.a. disposal) of this websocket. + /// + /// Async disposal as recommended by Stephen Cleary (https://blog.stephencleary.com/2013/03/async-oop-6-disposal.html) + public Task Completion { get; private set; } + + private readonly object completedLocker = new object(); + private async Task CompleteAsync() { Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}..."); if (!_cancellationTokenSource.IsCancellationRequested) _cancellationTokenSource.Cancel(); diff --git a/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj b/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj index 583a8e82..b61c1466 100644 --- a/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj +++ b/tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj @@ -2,8 +2,7 @@ netcoreapp3.1 - - false + false diff --git a/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj index 8526c03a..7b8d4982 100644 --- a/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj +++ b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj @@ -2,6 +2,7 @@ netstandard2.0 + false From 72500b51c4dbc127586aaf95a7d12239d14e7089 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 17 Feb 2020 10:48:20 +0100 Subject: [PATCH 26/55] cleanup websocket code --- .../Websocket/GraphQLHttpWebSocket.cs | 133 +++++++++--------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 2b2f1db4..2789a305 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -14,57 +14,57 @@ namespace GraphQL.Client.Http.Websocket { internal class GraphQLHttpWebSocket : IDisposable { private readonly Uri webSocketUri; - private readonly GraphQLHttpClientOptions _options; + private readonly GraphQLHttpClientOptions options; private readonly ArraySegment buffer; - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - private Subject _responseSubject; - private readonly Subject _requestSubject = new Subject(); - private readonly Subject _exceptionSubject = new Subject(); - private readonly IDisposable _requestSubscription; + private Subject responseSubject; + private readonly Subject requestSubject = new Subject(); + private readonly Subject exceptionSubject = new Subject(); + private readonly IDisposable requestSubscription; public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None; + public IObservable ReceiveErrors => exceptionSubject.AsObservable(); + public IObservable ResponseStream { get; } #if NETFRAMEWORK private WebSocket clientWebSocket = null; #else private ClientWebSocket clientWebSocket = null; #endif - private int _connectionAttempt = 0; + private int connectionAttempt = 0; public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClientOptions options) { this.webSocketUri = webSocketUri; - _options = options; + this.options = options; buffer = new ArraySegment(new byte[8192]); - _responseStream = _createResponseStream(); + ResponseStream = _createResponseStream(); - _requestSubscription = _requestSubject.Select(request => Observable.FromAsync(() => _sendWebSocketRequest(request))).Concat().Subscribe(); + requestSubscription = requestSubject.Select(request => Observable.FromAsync(() => _sendWebSocketRequest(request))).Concat().Subscribe(); } - public IObservable ReceiveErrors => _exceptionSubject.AsObservable(); - public IObservable ResponseStream => _responseStream; - public readonly IObservable _responseStream; + #region Send requests public Task SendWebSocketRequest(GraphQLWebSocketRequest request) { - _requestSubject.OnNext(request); + requestSubject.OnNext(request); return request.SendTask(); } private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request) { try { - if (_cancellationTokenSource.Token.IsCancellationRequested) { + if (cancellationTokenSource.Token.IsCancellationRequested) { request.SendCanceled(); return; } await InitializeWebSocket().ConfigureAwait(false); - var requestBytes = _options.JsonSerializer.SerializeToBytes(request); + var requestBytes = options.JsonSerializer.SerializeToBytes(request); await this.clientWebSocket.SendAsync( new ArraySegment(requestBytes), WebSocketMessageType.Text, true, - _cancellationTokenSource.Token).ConfigureAwait(false); + cancellationTokenSource.Token).ConfigureAwait(false); request.SendCompleted(); } catch (Exception e) { @@ -72,28 +72,18 @@ await this.clientWebSocket.SendAsync( } } - public Task InitializeWebSocketTask { get; private set; } = Task.CompletedTask; - - private readonly object _initializeLock = new object(); - -#region Private Methods - - private Task _backOff() { - _connectionAttempt++; - - if (_connectionAttempt == 1) return Task.CompletedTask; + #endregion - var delay = _options.BackOffStrategy(_connectionAttempt - 1); - Debug.WriteLine($"connection attempt #{_connectionAttempt}, backing off for {delay.TotalSeconds} s"); - return Task.Delay(delay); - } + public Task InitializeWebSocketTask { get; private set; } = Task.CompletedTask; + private readonly object initializeLock = new object(); + public Task InitializeWebSocket() { // do not attempt to initialize if cancellation is requested if (Completion != null) throw new OperationCanceledException(); - lock (_initializeLock) { + lock (initializeLock) { // if an initialization task is already running, return that if (InitializeWebSocketTask != null && !InitializeWebSocketTask.IsFaulted && @@ -115,13 +105,13 @@ public Task InitializeWebSocket() { switch (clientWebSocket) { case ClientWebSocket nativeWebSocket: nativeWebSocket.Options.AddSubProtocol("graphql-ws"); - nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)_options.HttpMessageHandler).ClientCertificates; - nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)_options.HttpMessageHandler).UseDefaultCredentials; + nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)options.HttpMessageHandler).ClientCertificates; + nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)options.HttpMessageHandler).UseDefaultCredentials; break; case System.Net.WebSockets.Managed.ClientWebSocket managedWebSocket: managedWebSocket.Options.AddSubProtocol("graphql-ws"); - managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)_options.HttpMessageHandler).ClientCertificates; - managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)_options.HttpMessageHandler).UseDefaultCredentials; + managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)options.HttpMessageHandler).ClientCertificates; + managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)options.HttpMessageHandler).UseDefaultCredentials; break; default: throw new NotSupportedException($"unknown websocket type {clientWebSocket.GetType().Name}"); @@ -129,10 +119,10 @@ public Task InitializeWebSocket() { #else clientWebSocket = new ClientWebSocket(); clientWebSocket.Options.AddSubProtocol("graphql-ws"); - clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)_options.HttpMessageHandler).ClientCertificates; - clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)_options.HttpMessageHandler).UseDefaultCredentials; + clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)options.HttpMessageHandler).ClientCertificates; + clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)options.HttpMessageHandler).UseDefaultCredentials; #endif - return InitializeWebSocketTask = _connectAsync(_cancellationTokenSource.Token); + return InitializeWebSocketTask = _connectAsync(cancellationTokenSource.Token); } } @@ -144,25 +134,25 @@ private IObservable _createResponseStream() { } private async Task _createResultStream(IObserver observer, CancellationToken token) { - if (_responseSubject == null || _responseSubject.IsDisposed) { - _responseSubject = new Subject(); + if (responseSubject == null || responseSubject.IsDisposed) { + responseSubject = new Subject(); var observable = await _getReceiveResultStream().ConfigureAwait(false); - observable.Subscribe(_responseSubject); + observable.Subscribe(responseSubject); - _responseSubject.Subscribe(_ => { }, ex => { - _exceptionSubject.OnNext(ex); - _responseSubject?.Dispose(); - _responseSubject = null; + responseSubject.Subscribe(_ => { }, ex => { + exceptionSubject.OnNext(ex); + responseSubject?.Dispose(); + responseSubject = null; }, () => { - _responseSubject?.Dispose(); - _responseSubject = null; + responseSubject?.Dispose(); + responseSubject = null; }); } return new CompositeDisposable ( - _responseSubject.Subscribe(observer), + responseSubject.Subscribe(observer), Disposable.Create(() => { Debug.WriteLine("response stream disposed"); }) @@ -180,30 +170,39 @@ private async Task _connectAsync(CancellationToken token) { Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}"); - _connectionAttempt = 1; + connectionAttempt = 1; } catch (Exception e) { - _exceptionSubject.OnNext(e); + exceptionSubject.OnNext(e); throw; } } + + 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); + } - private Task _receiveAsyncTask = null; - private readonly object _receiveTaskLocker = new object(); + 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() { - lock (_receiveTaskLocker) { - if (_receiveAsyncTask == null || - _receiveAsyncTask.IsFaulted || - _receiveAsyncTask.IsCompleted) - _receiveAsyncTask = _receiveResultAsync(); + lock (receiveTaskLocker) { + if (receiveAsyncTask == null || + receiveAsyncTask.IsFaulted || + receiveAsyncTask.IsCompleted) + receiveAsyncTask = _receiveResultAsync(); } - return _receiveAsyncTask; + return receiveAsyncTask; } private async Task _receiveResultAsync() { @@ -213,17 +212,17 @@ private async Task _receiveResultAsync() { using (var ms = new MemoryStream()) { WebSocketReceiveResult webSocketReceiveResult = null; do { - _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + cancellationTokenSource.Token.ThrowIfCancellationRequested(); webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); } while (!webSocketReceiveResult.EndOfMessage); - _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + cancellationTokenSource.Token.ThrowIfCancellationRequested(); ms.Seek(0, SeekOrigin.Begin); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { - var response = await _options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); + var response = await options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); response.MessageBytes = ms.ToArray(); return response; } @@ -254,8 +253,6 @@ private async Task _closeAsync(CancellationToken cancellationToken = default) { await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cancellationToken).ConfigureAwait(false); } - #endregion - #region IDisposable public void Dispose() => Complete(); @@ -277,12 +274,12 @@ 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 (!cancellationTokenSource.IsCancellationRequested) + cancellationTokenSource.Cancel(); await _closeAsync().ConfigureAwait(false); - _requestSubscription?.Dispose(); + requestSubscription?.Dispose(); clientWebSocket?.Dispose(); - _cancellationTokenSource.Dispose(); + cancellationTokenSource.Dispose(); Debug.WriteLine($"websocket {clientWebSocket.GetHashCode()} disposed"); } #endregion From 56bde94867dcaaea8210ddc404e581be637f2ef6 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 17 Feb 2020 11:00:46 +0100 Subject: [PATCH 27/55] add some comments --- .../Websocket/GraphQLHttpWebSocket.cs | 98 ++++++++++--------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 2789a305..4efa0923 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -17,22 +17,23 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly GraphQLHttpClientOptions options; private readonly ArraySegment buffer; private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - - private Subject responseSubject; private readonly Subject requestSubject = new Subject(); private readonly Subject exceptionSubject = new Subject(); private readonly IDisposable requestSubscription; - public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None; - public IObservable ReceiveErrors => exceptionSubject.AsObservable(); - public IObservable ResponseStream { get; } + private int connectionAttempt = 0; + private Subject responseSubject; #if NETFRAMEWORK private WebSocket clientWebSocket = null; #else private ClientWebSocket clientWebSocket = null; #endif - private int connectionAttempt = 0; + + + public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None; + public IObservable ReceiveErrors => exceptionSubject.AsObservable(); + public IObservable ResponseStream { get; } public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClientOptions options) { this.webSocketUri = webSocketUri; @@ -74,8 +75,7 @@ await this.clientWebSocket.SendAsync( #endregion - public Task InitializeWebSocketTask { get; private set; } = Task.CompletedTask; - + private Task initializeWebSocketTask = Task.CompletedTask; private readonly object initializeLock = new object(); public Task InitializeWebSocket() { @@ -85,10 +85,10 @@ public Task InitializeWebSocket() { lock (initializeLock) { // if an initialization task is already running, return that - if (InitializeWebSocketTask != null && - !InitializeWebSocketTask.IsFaulted && - !InitializeWebSocketTask.IsCompleted) - return InitializeWebSocketTask; + if (initializeWebSocketTask != null && + !initializeWebSocketTask.IsFaulted && + !initializeWebSocketTask.IsCompleted) + return initializeWebSocketTask; // if the websocket is open, return a completed task if (clientWebSocket != null && clientWebSocket.State == WebSocketState.Open) @@ -122,10 +122,39 @@ 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(cancellationTokenSource.Token); } } + private async Task _connectAsync(CancellationToken token) { + try { + await _backOff().ConfigureAwait(false); + Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); + await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); + Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}"); + connectionAttempt = 1; + } + catch (Exception e) { + exceptionSubject.OnNext(e); + throw; + } + } + + /// + /// delay the next connection attempt using + /// + /// + 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); + } + + private IObservable _createResponseStream() { return Observable.Create(_createResultStream) // complete sequence on OperationCanceledException, this is triggered by the cancellation token on disposal @@ -135,10 +164,16 @@ private IObservable _createResponseStream() { private async Task _createResultStream(IObserver observer, CancellationToken token) { if (responseSubject == null || responseSubject.IsDisposed) { + // create new response subject responseSubject = new Subject(); - var observable = await _getReceiveResultStream().ConfigureAwait(false); - observable.Subscribe(responseSubject); + // 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); + + // dispose the subject on any error or completion (will be recreated) responseSubject.Subscribe(_ => { }, ex => { exceptionSubject.OnNext(ex); responseSubject?.Dispose(); @@ -159,35 +194,6 @@ private async Task _createResultStream(IObserver> _getReceiveResultStream() { - await InitializeWebSocket().ConfigureAwait(false); - return Observable.Defer(() => _getReceiveTask().ToObservable()).Repeat(); - } - - private async Task _connectAsync(CancellationToken token) { - try { - await _backOff().ConfigureAwait(false); - Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); - await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); - Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}"); - connectionAttempt = 1; - } - catch (Exception e) { - exceptionSubject.OnNext(e); - throw; - } - } - - 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); - } - private Task receiveAsyncTask = null; private readonly object receiveTaskLocker = new object(); /// @@ -205,6 +211,10 @@ private Task _getReceiveTask() { return receiveAsyncTask; } + /// + /// read a single message from the websocket + /// + /// private async Task _receiveResultAsync() { try { Debug.WriteLine($"receiving data on websocket {clientWebSocket.GetHashCode()} ..."); From 34010007db664be07833b0bcd26a427b4a91eb47 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 17 Feb 2020 11:14:38 +0100 Subject: [PATCH 28/55] create observable for the websockets connection state --- .../GraphQLWebsocketConnectionState.cs | 7 +++++++ src/GraphQL.Client/GraphQLHttpClient.cs | 6 +++++- src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs | 11 ++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/GraphQL.Client.Abstractions.Websocket/GraphQLWebsocketConnectionState.cs diff --git a/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebsocketConnectionState.cs b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebsocketConnectionState.cs new file mode 100644 index 00000000..3ab5a0e2 --- /dev/null +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebsocketConnectionState.cs @@ -0,0 +1,7 @@ +namespace GraphQL.Client.Abstractions.Websocket { + public enum GraphQLWebsocketConnectionState { + Disconnected, + Connecting, + Connected + } +} diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 465a0286..a387aa66 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Linq; using System.Net.Http; using System.Text; using System.Threading; @@ -33,6 +32,11 @@ public class GraphQLHttpClient : IGraphQLClient { /// public IObservable WebSocketReceiveErrors => graphQlHttpWebSocket.ReceiveErrors; + /// + /// the websocket connection state + /// + public IObservable WebsocketConnectionState => + graphQlHttpWebSocket.ConnectionState; #region Constructors diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 4efa0923..0e44377f 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -19,6 +19,8 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private readonly Subject requestSubject = new Subject(); private readonly Subject exceptionSubject = new Subject(); + private readonly BehaviorSubject stateSubject = + new BehaviorSubject(GraphQLWebsocketConnectionState.Disconnected); private readonly IDisposable requestSubscription; private int connectionAttempt = 0; @@ -33,6 +35,8 @@ internal class GraphQLHttpWebSocket : IDisposable { 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, GraphQLHttpClientOptions options) { @@ -95,8 +99,8 @@ public Task InitializeWebSocket() { return Task.CompletedTask; // else (re-)create websocket and connect - //_responseStreamConnection?.Dispose(); clientWebSocket?.Dispose(); + stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting); #if NETFRAMEWORK // fix websocket not supported on win 7 using @@ -131,10 +135,12 @@ private async Task _connectAsync(CancellationToken token) { await _backOff().ConfigureAwait(false); Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); + stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected); Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}"); connectionAttempt = 1; } catch (Exception e) { + stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); exceptionSubject.OnNext(e); throw; } @@ -178,10 +184,12 @@ private async Task _createResultStream(IObserver { responseSubject?.Dispose(); responseSubject = null; + stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); }); } @@ -261,6 +269,7 @@ private async Task _closeAsync(CancellationToken cancellationToken = default) { Debug.WriteLine($"closing websocket {clientWebSocket.GetHashCode()}"); await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cancellationToken).ConfigureAwait(false); + stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); } #region IDisposable From c546271770a54758b03dd56f254d5b5ffae34e16 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 17 Feb 2020 11:29:36 +0100 Subject: [PATCH 29/55] add OnWebsocketConnected callback --- src/GraphQL.Client/GraphQLHttpClient.cs | 4 +-- .../GraphQLHttpClientOptions.cs | 5 ++++ .../Websocket/GraphQLHttpWebSocket.cs | 29 ++++++++++--------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index a387aa66..dbf4bcb7 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -51,7 +51,7 @@ public GraphQLHttpClient(Action configure) : this(conf public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient) { Options = options; this.HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); + this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), this); Options.JsonSerializer = JsonSerializer.EnsureAssigned(); } @@ -59,7 +59,7 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient Options = options ?? throw new ArgumentNullException(nameof(options)); Options.JsonSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); this.HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options); + this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), this); } #endregion diff --git a/src/GraphQL.Client/GraphQLHttpClientOptions.cs b/src/GraphQL.Client/GraphQLHttpClientOptions.cs index 8a9afae8..f6b2b6eb 100644 --- a/src/GraphQL.Client/GraphQLHttpClientOptions.cs +++ b/src/GraphQL.Client/GraphQLHttpClientOptions.cs @@ -49,5 +49,10 @@ public class GraphQLHttpClientOptions { /// Request preprocessing function. Can be used i.e. to inject authorization info into a GraphQL request payload. /// public Func> PreprocessRequest { get; set; } = (request, client) => Task.FromResult(request); + + /// + /// This function is called after successfully establishing a websocket connection but before any regular request is made. + /// + public Func OnWebsocketConnected { get; set; } = client => Task.CompletedTask; } } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 0e44377f..b82ac252 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -14,7 +14,7 @@ namespace GraphQL.Client.Http.Websocket { internal class GraphQLHttpWebSocket : IDisposable { private readonly Uri webSocketUri; - private readonly GraphQLHttpClientOptions options; + private readonly GraphQLHttpClient client; private readonly ArraySegment buffer; private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private readonly Subject requestSubject = new Subject(); @@ -25,6 +25,7 @@ internal class GraphQLHttpWebSocket : IDisposable { private int connectionAttempt = 0; private Subject responseSubject; + private GraphQLHttpClientOptions Options => client.Options; #if NETFRAMEWORK private WebSocket clientWebSocket = null; @@ -39,9 +40,9 @@ internal class GraphQLHttpWebSocket : IDisposable { public IObservable ResponseStream { get; } - public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClientOptions options) { + public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { this.webSocketUri = webSocketUri; - this.options = options; + this.client = client; buffer = new ArraySegment(new byte[8192]); ResponseStream = _createResponseStream(); @@ -64,7 +65,7 @@ private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request) { } await InitializeWebSocket().ConfigureAwait(false); - var requestBytes = options.JsonSerializer.SerializeToBytes(request); + var requestBytes = Options.JsonSerializer.SerializeToBytes(request); await this.clientWebSocket.SendAsync( new ArraySegment(requestBytes), WebSocketMessageType.Text, @@ -109,13 +110,13 @@ public Task InitializeWebSocket() { switch (clientWebSocket) { case ClientWebSocket nativeWebSocket: nativeWebSocket.Options.AddSubProtocol("graphql-ws"); - nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)options.HttpMessageHandler).ClientCertificates; - nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)options.HttpMessageHandler).UseDefaultCredentials; + nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; + nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; break; case System.Net.WebSockets.Managed.ClientWebSocket managedWebSocket: managedWebSocket.Options.AddSubProtocol("graphql-ws"); - managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)options.HttpMessageHandler).ClientCertificates; - managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)options.HttpMessageHandler).UseDefaultCredentials; + managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; + managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; break; default: throw new NotSupportedException($"unknown websocket type {clientWebSocket.GetType().Name}"); @@ -123,8 +124,8 @@ public Task InitializeWebSocket() { #else clientWebSocket = new ClientWebSocket(); clientWebSocket.Options.AddSubProtocol("graphql-ws"); - clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)options.HttpMessageHandler).ClientCertificates; - clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)options.HttpMessageHandler).UseDefaultCredentials; + clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; + clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; #endif return initializeWebSocketTask = _connectAsync(cancellationTokenSource.Token); } @@ -136,7 +137,9 @@ private async Task _connectAsync(CancellationToken token) { Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected); - Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}"); + 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; } catch (Exception e) { @@ -155,7 +158,7 @@ private Task _backOff() { if (connectionAttempt == 1) return Task.CompletedTask; - var delay = options.BackOffStrategy?.Invoke(connectionAttempt - 1) ?? TimeSpan.FromSeconds(5); + 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); } @@ -240,7 +243,7 @@ private async Task _receiveResultAsync() { ms.Seek(0, SeekOrigin.Begin); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { - var response = await options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); + var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); response.MessageBytes = ms.ToArray(); return response; } From 1080536408d34ccae4a99ad4a2235b324dbcaa49 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 18 Feb 2020 15:05:55 +0100 Subject: [PATCH 30/55] add tests for callback and WebsocketConnectionState --- .../GraphQL.Client.Tests.Common.csproj | 1 + .../Helpers/CallbackMonitor.cs | 115 ++++++++++++++++++ .../Helpers/CallbackTester.cs | 67 ---------- .../Helpers/MiscellaneousExtensions.cs | 14 +++ .../QueryAndMutationTests/Base.cs | 4 +- .../WebsocketTests/Base.cs | 94 ++++++++------ 6 files changed, 186 insertions(+), 109 deletions(-) create mode 100644 tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs delete mode 100644 tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs diff --git a/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj index 7b8d4982..dfbb9146 100644 --- a/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj +++ b/tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj @@ -17,6 +17,7 @@ + diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs new file mode 100644 index 00000000..4b623ac6 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs @@ -0,0 +1,115 @@ +using System; +using System.Threading; +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Primitives; + +namespace GraphQL.Client.Tests.Common.Helpers { + public class CallbackMonitor { + private readonly ManualResetEventSlim callbackInvoked = new ManualResetEventSlim(); + + /// + /// The timeout for . Defaults to 1 s + /// + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Indicates that an update has been received since the last + /// + public bool CallbackInvoked => callbackInvoked.IsSet; + /// + /// The last payload which was received. + /// + public T LastPayload { get; private set; } + + public void Invoke(T param) { + LastPayload = param; + 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 + /// + public void Reset() { + LastPayload = default(T); + callbackInvoked.Reset(); + } + + + public CallbackAssertions Should() { + return new CallbackAssertions(this); + } + + public class CallbackAssertions : ReferenceTypeAssertions, CallbackAssertions> { + public CallbackAssertions(CallbackMonitor tester) { + Subject = tester; + } + + protected override string Identifier => "callback"; + + public AndWhichConstraint, TPayload> HaveBeenInvokedWithPayload(TimeSpan timeout, + string because = "", params object[] becauseArgs) { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject.callbackInvoked.Wait(timeout)) + .ForCondition(isSet => isSet) + .FailWith("Expected {context:callback} to be invoked{reason}, but did not receive a call within {0}", timeout); + + Subject.callbackInvoked.Reset(); + return new AndWhichConstraint, TPayload>(this, Subject.LastPayload); + } + public AndWhichConstraint, TPayload> HaveBeenInvokedWithPayload(string because = "", params object[] becauseArgs) + => HaveBeenInvokedWithPayload(Subject.Timeout, because, becauseArgs); + + public AndConstraint> HaveBeenInvoked(TimeSpan timeout, string because = "", params object[] becauseArgs) + => HaveBeenInvokedWithPayload(timeout, because, becauseArgs); + public AndConstraint> HaveBeenInvoked(string because = "", params object[] becauseArgs) + => HaveBeenInvokedWithPayload(Subject.Timeout, because, becauseArgs); + + public AndConstraint> NotHaveBeenInvoked(TimeSpan timeout, + string because = "", params object[] becauseArgs) { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject.callbackInvoked.Wait(timeout)) + .ForCondition(isSet => !isSet) + .FailWith("Expected {context:callback} to not be invoked{reason}, but did receive a call: {0}", Subject.LastPayload); + + Subject.callbackInvoked.Reset(); + return new AndConstraint>(this); + } + public AndConstraint> NotHaveBeenInvoked(string because = "", params object[] becauseArgs) + => NotHaveBeenInvoked(TimeSpan.FromMilliseconds(100), because, becauseArgs); + } + } +} diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs deleted file mode 100644 index c8ca29c5..00000000 --- a/tests/GraphQL.Client.Tests.Common/Helpers/CallbackTester.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Threading; -using FluentAssertions; - -namespace GraphQL.Client.Tests.Common.Helpers { - public class CallbackTester { - private ManualResetEventSlim _callbackInvoked { get; } = new ManualResetEventSlim(); - - /// - /// The timeout for . Defaults to 1 s - /// - public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1); - - /// - /// Indicates that an update has been received since the last - /// - public bool CallbackInvoked => _callbackInvoked.IsSet; - /// - /// The last payload which was received. - /// - public T LastPayload { get; private set; } - - public void Callback(T param) { - LastPayload = param; - _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 - /// - public void Reset() { - LastPayload = default(T); - _callbackInvoked.Reset(); - } - } -} diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/MiscellaneousExtensions.cs b/tests/GraphQL.Client.Tests.Common/Helpers/MiscellaneousExtensions.cs index 2da34009..0e254672 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/MiscellaneousExtensions.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/MiscellaneousExtensions.cs @@ -1,4 +1,6 @@ using System.Linq; +using System.Threading.Tasks; +using GraphQL.Client.Http; namespace GraphQL.Client.Tests.Common.Helpers { public static class MiscellaneousExtensions { @@ -7,5 +9,17 @@ public static string RemoveWhitespace(this string input) { .Where(c => !char.IsWhiteSpace(c)) .ToArray()); } + + public static CallbackMonitor ConfigureMonitorForOnWebsocketConnected( + this GraphQLHttpClient client) { + var tester = new CallbackMonitor(); + client.Options.OnWebsocketConnected = c => { + tester.Invoke(c); + return Task.CompletedTask; + }; + return tester; + } + + } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index be153ba2..16e73c92 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -139,9 +139,9 @@ query Human($id: String!){ [Fact] public async void PreprocessHttpRequestMessageIsCalled() { - var callbackTester = new CallbackTester(); + var callbackTester = new CallbackMonitor(); var graphQLRequest = new GraphQLHttpRequest($"{{ human(id: \"1\") {{ name }} }}") { - PreprocessHttpRequestMessage = callbackTester.Callback + PreprocessHttpRequestMessage = callbackTester.Invoke }; using (var setup = SetupTest()) { diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index ff1cabe1..fd2d6ffb 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -16,24 +16,20 @@ namespace GraphQL.Integration.Tests.WebsocketTests { public abstract class Base { - protected readonly ITestOutputHelper output; - protected readonly IGraphQLWebsocketJsonSerializer serializer; + protected readonly ITestOutputHelper Output; + protected readonly IGraphQLWebsocketJsonSerializer Serializer; protected IWebHost CreateServer(int port) => WebHostHelpers.CreateServer(port); - public Base(ITestOutputHelper output, IGraphQLWebsocketJsonSerializer serializer) { - this.output = output; - this.serializer = serializer; - } - - public Base(ITestOutputHelper output) { - this.output = output; + protected Base(ITestOutputHelper output, IGraphQLWebsocketJsonSerializer serializer) { + this.Output = output; + this.Serializer = serializer; } [Fact] public async void AssertTestingHarness() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); + var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); const string message = "some random testing message"; var response = await client.AddMessageAsync(message).ConfigureAwait(false); @@ -47,7 +43,7 @@ public async void AssertTestingHarness() { public async void CanSendRequestViaWebsocket() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, true, serializer); + var client = WebHostHelpers.GetGraphQLClient(port, true, Serializer); const string message = "some random testing message"; var response = await client.AddMessageAsync(message).ConfigureAwait(false); @@ -59,7 +55,7 @@ public async void CanSendRequestViaWebsocket() { public async void CanHandleRequestErrorViaWebsocket() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, true, serializer); + var client = WebHostHelpers.GetGraphQLClient(port, true, Serializer); var response = await client.SendQueryAsync("this query is formatted quite badly").ConfigureAwait(false); Assert.Single(response.Errors); @@ -79,9 +75,11 @@ public async void CanHandleRequestErrorViaWebsocket() { [Fact] public async void CanCreateObservableSubscription() { var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); + 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); @@ -121,14 +119,15 @@ public class MessageAddedContent { public async void CanReconnectWithSameObservable() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); - await client.InitializeWebsocketConnection(); + var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); + var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); Debug.WriteLine("creating subscription stream"); - IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest); + 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); @@ -143,9 +142,7 @@ public async void CanReconnectWithSameObservable() { .Which.Data.MessageAdded.Content.Should().Be(message2); Debug.WriteLine("disposing subscription..."); - tester.Dispose(); - await Task.Delay(500); - await client.InitializeWebsocketConnection(); + tester.Dispose(); // does not close the websocket connection Debug.WriteLine("creating new subscription..."); tester = observable.Monitor(); @@ -188,17 +185,19 @@ public class UserJoinedContent { [Fact] public async void CanConnectTwoSubscriptionsSimultaneously() { var port = NetworkHelpers.GetFreeTcpPortNumber(); - var callbackTester = new CallbackTester(); - var callbackTester2 = new CallbackTester(); + var callbackTester = new CallbackMonitor(); + var callbackTester2 = new CallbackMonitor(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); + 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.Callback); + client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Invoke); IObservable> observable2 = - client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Callback); + client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Invoke); Debug.WriteLine("subscribing..."); var tester = observable1.Monitor(); @@ -237,15 +236,24 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { public async void CanHandleConnectionTimeout() { var port = NetworkHelpers.GetFreeTcpPortNumber(); var server = CreateServer(port); - var callbackTester = new CallbackTester(); + var errorMonitor = new CallbackMonitor(); + + var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); + var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); + var statusMonitor = client.WebsocketConnectionState.Monitor(); + statusMonitor.Should().HaveReceivedPayload().Which.Should() + .Be(GraphQLWebsocketConnectionState.Disconnected); - var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); - await client.InitializeWebsocketConnection(); Debug.WriteLine("creating subscription stream"); - IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Callback); + IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest, errorMonitor.Invoke); Debug.WriteLine("subscribing..."); var tester = observable.Monitor(); + statusMonitor.Should().HaveReceivedPayload().Which.Should() + .Be(GraphQLWebsocketConnectionState.Connecting); + statusMonitor.Should().HaveReceivedPayload().Which.Should() + .Be(GraphQLWebsocketConnectionState.Connected); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); const string message1 = "Hello World"; var response = await client.AddMessageAsync(message1).ConfigureAwait(false); @@ -255,18 +263,20 @@ public async void CanHandleConnectionTimeout() { Debug.WriteLine("stopping web host..."); await server.StopAsync(CancellationToken.None).ConfigureAwait(false); + server.Dispose(); Debug.WriteLine("web host stopped..."); - callbackTester.CallbackShouldHaveBeenInvoked(exception => { - Assert.IsType(exception); - }, TimeSpan.FromSeconds(10)); + errorMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)) + .Which.Should().BeOfType(); + statusMonitor.Should().HaveReceivedPayload().Which.Should() + .Be(GraphQLWebsocketConnectionState.Disconnected); - try { - server.Start(); - } - catch (Exception e) { - output.WriteLine($"failed to restart server: {e}"); - } + server = CreateServer(port); + statusMonitor.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)).Which.Should() + .Be(GraphQLWebsocketConnectionState.Connecting); + statusMonitor.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)).Which.Should() + .Be(GraphQLWebsocketConnectionState.Connected); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); // disposing the client should complete the subscription client.Dispose(); @@ -279,8 +289,10 @@ public async void CanHandleConnectionTimeout() { public async void CanHandleSubscriptionError() { var port = NetworkHelpers.GetFreeTcpPortNumber(); using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); + 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(@" @@ -309,8 +321,10 @@ public async void CanHandleQueryErrorInSubscription() { var test = new GraphQLRequest("tset", new { test = "blaa" }); - var client = WebHostHelpers.GetGraphQLClient(port, serializer: serializer); + 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(@" From e12ea5bb6e48e65feddd61031c1af1226a6908c4 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 18 Feb 2020 16:22:23 +0100 Subject: [PATCH 31/55] fix "Connecting" status --- src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs | 2 +- tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index b82ac252..832fb4cf 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -101,7 +101,6 @@ public Task InitializeWebSocket() { // else (re-)create websocket and connect clientWebSocket?.Dispose(); - stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting); #if NETFRAMEWORK // fix websocket not supported on win 7 using @@ -134,6 +133,7 @@ public Task InitializeWebSocket() { private async Task _connectAsync(CancellationToken token) { try { await _backOff().ConfigureAwait(false); + stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting); Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index fd2d6ffb..7e0e06a2 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -237,9 +237,16 @@ 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(); + // configure back-off strategy to allow it to be controlled from within the unit test + client.Options.BackOffStrategy = i => { + reconnectBlocker.Wait(); + return TimeSpan.Zero; + }; + var statusMonitor = client.WebsocketConnectionState.Monitor(); statusMonitor.Should().HaveReceivedPayload().Which.Should() .Be(GraphQLWebsocketConnectionState.Disconnected); @@ -272,6 +279,7 @@ public async void CanHandleConnectionTimeout() { .Be(GraphQLWebsocketConnectionState.Disconnected); server = CreateServer(port); + reconnectBlocker.Set(); statusMonitor.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)).Which.Should() .Be(GraphQLWebsocketConnectionState.Connecting); statusMonitor.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)).Which.Should() From 40c910806f8f8a8d15fbb0bf3ad9f4fcd0cfd18a Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 18 Feb 2020 16:24:00 +0100 Subject: [PATCH 32/55] fix doc comment in src/GraphQL.Client/GraphQLHttpClientOptions.cs Co-Authored-By: Ivan Maximov --- src/GraphQL.Client/GraphQLHttpClientOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GraphQL.Client/GraphQLHttpClientOptions.cs b/src/GraphQL.Client/GraphQLHttpClientOptions.cs index f6b2b6eb..a52c772c 100644 --- a/src/GraphQL.Client/GraphQLHttpClientOptions.cs +++ b/src/GraphQL.Client/GraphQLHttpClientOptions.cs @@ -51,7 +51,7 @@ public class GraphQLHttpClientOptions { public Func> PreprocessRequest { get; set; } = (request, client) => Task.FromResult(request); /// - /// This function is called after successfully establishing a websocket connection but before any regular request is made. + /// This callback is called after successfully establishing a websocket connection but before any regular request is made. /// public Func OnWebsocketConnected { get; set; } = client => Task.CompletedTask; } From d276793d1280324b1bd278d89ff13b267f075980 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 18 Feb 2020 16:48:17 +0100 Subject: [PATCH 33/55] extend timeout on CanHandleSubscriptionError test --- tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 7e0e06a2..b17ba3c0 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -313,7 +313,7 @@ public async void CanHandleSubscriptionError() { Debug.WriteLine("subscribing..."); using (var tester = observable.Monitor()) { - tester.Should().HaveReceivedPayload() + tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) .Which.Errors.Should().ContainSingle(); tester.Should().HaveCompleted(); client.Dispose(); From 03d22cef8d37b19645498e9a6af97c3bfdb2d85d Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 10:54:46 +0100 Subject: [PATCH 34/55] create branche workflow for ubuntu --- .github/workflows/branches-ubuntu.yml | 80 ++++++++++++++++++++++++ GraphQL.Client.sln | 6 +- src/GraphQL.Client/GraphQL.Client.csproj | 2 +- 3 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/branches-ubuntu.yml diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml new file mode 100644 index 00000000..f9b89c25 --- /dev/null +++ b/.github/workflows/branches-ubuntu.yml @@ -0,0 +1,80 @@ +name: Branch workflow (Ubuntu) +on: + push: + branches-ignore: + - develop + - 'release/**' + - 'releases/**' +jobs: + generateVersionInfo: + name: GenerateVersionInfo + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup dotnet environment + uses: actions/setup-dotnet@master + with: + dotnet-version: '3.1.100' + - name: Restore dotnet tools + run: dotnet tool restore + - name: Fetch complete repository + run: git fetch + - name: Generate version info from git history + run: dotnet gitversion /output json | jq -r 'to_entries|map("GitVersion_\(.key)=\(.value|tostring)")|.[]' > gitversion.properties + - name: Upload version info file + uses: actions/upload-artifact@v1 + with: + name: gitversion + path: gitversion.properties + + build: + name: Build + needs: generateVersionInfo + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup dotnet environment + uses: actions/setup-dotnet@master + with: + dotnet-version: '3.1.100' + - name: Download version info file + uses: actions/download-artifact@v1 + with: + name: gitversion + path: ./ + - name: Inject version info into environment + run: export $(cat gitversion.properties | xargs) && echo $GitVersion_SemVer + - name: Build solution + run: dotnet build -c Release + - name: Create NuGet packages + run: dotnet pack -c Release --no-build -o nupkg + - name: Upload nuget packages + uses: actions/upload-artifact@v1 + with: + name: nupkg + path: nupkg + + test: + name: Test + needs: [build, generateVersionInfo] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup dotnet environment + uses: actions/setup-dotnet@master + with: + dotnet-version: '3.1.100' + - name: Download version info file + uses: actions/download-artifact@v1 + with: + name: gitversion + path: ./ + - name: Inject version info into environment + run: export $(cat gitversion.properties | xargs) && echo $GitVersion_SemVer + - name: Run tests + run: dotnet test -c Release diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln index 510fa0b0..69510eb7 100644 --- a/GraphQL.Client.sln +++ b/GraphQL.Client.sln @@ -32,9 +32,6 @@ 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}" - ProjectSection(SolutionItems) = preProject - .github\FUNDING.yml = .github\FUNDING.yml - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{05CAF9B2-981E-40C0-AE31-5FA56E351F12}" ProjectSection(SolutionItems) = preProject @@ -68,7 +65,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Serializer.T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Tests.Common", "tests\GraphQL.Client.Tests.Common\GraphQL.Client.Tests.Common.csproj", "{0D307BAD-27AE-4A5D-8764-4AA2620B01E9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.Serializer.SystemTextJson", "src\GraphQL.Client.Serializer.SystemTextJson\GraphQL.Client.Serializer.SystemTextJson.csproj", "{7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5}" +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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -138,6 +135,7 @@ Global 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} diff --git a/src/GraphQL.Client/GraphQL.Client.csproj b/src/GraphQL.Client/GraphQL.Client.csproj index 31fa21ab..d7e74c27 100644 --- a/src/GraphQL.Client/GraphQL.Client.csproj +++ b/src/GraphQL.Client/GraphQL.Client.csproj @@ -3,7 +3,7 @@ - netstandard2.0;net461 + netstandard2.0 GraphQL.Client.Http From bac49776025d22c6b7e8747e5203972e3c2a4d7f Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 11:01:08 +0100 Subject: [PATCH 35/55] inject environment using ::set-env --- .github/workflows/branches-ubuntu.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index f9b89c25..668067db 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -23,12 +23,12 @@ jobs: - name: Fetch complete repository run: git fetch - name: Generate version info from git history - run: dotnet gitversion /output json | jq -r 'to_entries|map("GitVersion_\(.key)=\(.value|tostring)")|.[]' > gitversion.properties + run: dotnet gitversion /output json > gitversion.json - name: Upload version info file uses: actions/upload-artifact@v1 with: name: gitversion - path: gitversion.properties + path: gitversion.json build: name: Build @@ -47,7 +47,7 @@ jobs: name: gitversion path: ./ - name: Inject version info into environment - run: export $(cat gitversion.properties | xargs) && echo $GitVersion_SemVer + run: jq -r 'to_entries|map("::set-env name=GitVersion_\(.key)::\(.value|tostring)")|.[]' gitversion.json - name: Build solution run: dotnet build -c Release - name: Create NuGet packages @@ -75,6 +75,6 @@ jobs: name: gitversion path: ./ - name: Inject version info into environment - run: export $(cat gitversion.properties | xargs) && echo $GitVersion_SemVer + run: jq -r 'to_entries|map("::set-env name=GitVersion_\(.key)::\(.value|tostring)")|.[]' gitversion.json - name: Run tests run: dotnet test -c Release From 5dae880228a9df6ab51d0a00ab6770d4cc106418 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 11:07:28 +0100 Subject: [PATCH 36/55] output current version before build --- .github/workflows/branches-ubuntu.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 668067db..7f066282 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -49,7 +49,7 @@ jobs: - name: Inject version info into environment run: jq -r 'to_entries|map("::set-env name=GitVersion_\(.key)::\(.value|tostring)")|.[]' gitversion.json - name: Build solution - run: dotnet build -c Release + run: echo "Current version is \"$GitVersion_SemVer\"" && dotnet build -c Release - name: Create NuGet packages run: dotnet pack -c Release --no-build -o nupkg - name: Upload nuget packages @@ -77,4 +77,4 @@ jobs: - name: Inject version info into environment run: jq -r 'to_entries|map("::set-env name=GitVersion_\(.key)::\(.value|tostring)")|.[]' gitversion.json - name: Run tests - run: dotnet test -c Release + run: echo "Current version is \"$GitVersion_SemVer\"" && dotnet test -c Release From b9710b3bfaccd2a7578d7f84393dd2e2e67a6df6 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 11:34:15 +0100 Subject: [PATCH 37/55] should run on ubuntu without installing dotnet first --- .github/workflows/branches-ubuntu.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 7f066282..4fde52ff 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -14,10 +14,10 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - - name: Setup dotnet environment - uses: actions/setup-dotnet@master - with: - dotnet-version: '3.1.100' + # - name: Setup dotnet environment + # uses: actions/setup-dotnet@master + # with: + # dotnet-version: '3.1.100' - name: Restore dotnet tools run: dotnet tool restore - name: Fetch complete repository @@ -37,10 +37,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Setup dotnet environment - uses: actions/setup-dotnet@master - with: - dotnet-version: '3.1.100' + # - name: Setup dotnet environment + # uses: actions/setup-dotnet@master + # with: + # dotnet-version: '3.1.100' - name: Download version info file uses: actions/download-artifact@v1 with: @@ -65,10 +65,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Setup dotnet environment - uses: actions/setup-dotnet@master - with: - dotnet-version: '3.1.100' + # - name: Setup dotnet environment + # uses: actions/setup-dotnet@master + # with: + # dotnet-version: '3.1.100' - name: Download version info file uses: actions/download-artifact@v1 with: From fcde0a4c0c35fe3b6bc9f73891f2aa848f2aaf9a Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 11:36:10 +0100 Subject: [PATCH 38/55] disable dotnet telemetry --- .github/workflows/branches-ubuntu.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 4fde52ff..086c7605 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -5,6 +5,8 @@ on: - develop - 'release/**' - 'releases/**' +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true jobs: generateVersionInfo: name: GenerateVersionInfo From 30d372b85b6e57b3d3dc898e8fe8ec1d728e8d46 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 11:41:55 +0100 Subject: [PATCH 39/55] remove commented dotnet setup steps --- .github/workflows/branches-ubuntu.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 086c7605..488f6596 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -16,10 +16,6 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - # - name: Setup dotnet environment - # uses: actions/setup-dotnet@master - # with: - # dotnet-version: '3.1.100' - name: Restore dotnet tools run: dotnet tool restore - name: Fetch complete repository @@ -39,10 +35,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - # - name: Setup dotnet environment - # uses: actions/setup-dotnet@master - # with: - # dotnet-version: '3.1.100' - name: Download version info file uses: actions/download-artifact@v1 with: @@ -67,10 +59,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - # - name: Setup dotnet environment - # uses: actions/setup-dotnet@master - # with: - # dotnet-version: '3.1.100' - name: Download version info file uses: actions/download-artifact@v1 with: From c2991b5104ff09e373cc3cd75e7a83841239288f Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 12:19:51 +0100 Subject: [PATCH 40/55] add ReferenceAssemblies package to allow net461 build on ubuntu vm --- src/GraphQL.Client/GraphQL.Client.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/GraphQL.Client/GraphQL.Client.csproj b/src/GraphQL.Client/GraphQL.Client.csproj index d7e74c27..3b11e942 100644 --- a/src/GraphQL.Client/GraphQL.Client.csproj +++ b/src/GraphQL.Client/GraphQL.Client.csproj @@ -3,7 +3,7 @@ - netstandard2.0 + netstandard2.0;net461 GraphQL.Client.Http @@ -26,6 +26,7 @@ + @@ -38,4 +39,7 @@ + + + From cde1924d7ff824b876ae34bc3e227e3425cefe46 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 12:26:23 +0100 Subject: [PATCH 41/55] rename type param to eliminate build warning --- .../Helpers/ObservableTester.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 1b8d8030..43bcbe18 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -7,7 +7,7 @@ using FluentAssertions.Primitives; namespace GraphQL.Client.Tests.Common.Helpers { - public class ObservableTester : IDisposable { + public class ObservableTester : IDisposable { private readonly IDisposable subscription; private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); @@ -25,7 +25,7 @@ public class ObservableTester : IDisposable { /// /// The last payload which was received. /// - public TPayload LastPayload { get; private set; } + public TSubscriptionPayload LastPayload { get; private set; } public Exception Error { get; private set; } @@ -33,7 +33,7 @@ public class ObservableTester : IDisposable { /// Creates a new which subscribes to the supplied /// /// the under test - public ObservableTester(IObservable observable) { + public ObservableTester(IObservable observable) { subscription = observable.ObserveOn(TaskPoolScheduler.Default).Subscribe( obj => { LastPayload = obj; @@ -59,8 +59,8 @@ public void Dispose() { subscription?.Dispose(); } - public SubscriptionAssertions Should() { - return new SubscriptionAssertions(this); + public SubscriptionAssertions Should() { + return new SubscriptionAssertions(this); } public class SubscriptionAssertions : ReferenceTypeAssertions, SubscriptionAssertions> { From 5a987990fb34e8068eab009bc659d4aed514ad2c Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 14:32:36 +0100 Subject: [PATCH 42/55] disable branch workflow on windows --- .github/workflows/branches.yml | 7 +------ GraphQL.Client.sln | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/branches.yml b/.github/workflows/branches.yml index a896d523..d88c4a2b 100644 --- a/.github/workflows/branches.yml +++ b/.github/workflows/branches.yml @@ -1,10 +1,5 @@ name: Branch workflow -on: - push: - branches-ignore: - - develop - - 'release/**' - - 'releases/**' +on: [] jobs: generateVersionInfo: name: GenerateVersionInfo diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln index 69510eb7..e1943dde 100644 --- a/GraphQL.Client.sln +++ b/GraphQL.Client.sln @@ -35,6 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{C421 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 .github\workflows\branches.yml = .github\workflows\branches.yml .github\workflows\main.yml = .github\workflows\main.yml EndProjectSection From 61b8adc7e5be81ae006fe16db2becaea42d50644 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 14:35:10 +0100 Subject: [PATCH 43/55] ignore all branches --- .github/workflows/branches.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/branches.yml b/.github/workflows/branches.yml index d88c4a2b..dbda74a2 100644 --- a/.github/workflows/branches.yml +++ b/.github/workflows/branches.yml @@ -1,5 +1,8 @@ name: Branch workflow -on: [] +on: + push: + branches-ignore: + - ** jobs: generateVersionInfo: name: GenerateVersionInfo From fc167577510855aa1c1514d96ee0e80c529ce007 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 14:36:02 +0100 Subject: [PATCH 44/55] put colons around wildcard --- .github/workflows/branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branches.yml b/.github/workflows/branches.yml index dbda74a2..430b2121 100644 --- a/.github/workflows/branches.yml +++ b/.github/workflows/branches.yml @@ -2,7 +2,7 @@ name: Branch workflow on: push: branches-ignore: - - ** + - '**' jobs: generateVersionInfo: name: GenerateVersionInfo From cd41f4b93ac07e2ae87d46a9919cb79a534b188a Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 15:01:53 +0100 Subject: [PATCH 45/55] fix connection status test --- .../WebsocketTests/Base.cs | 94 ++++++++++--------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index b17ba3c0..6112a00c 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Concurrent; 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; @@ -247,49 +247,55 @@ public async void CanHandleConnectionTimeout() { return TimeSpan.Zero; }; - var statusMonitor = client.WebsocketConnectionState.Monitor(); - statusMonitor.Should().HaveReceivedPayload().Which.Should() - .Be(GraphQLWebsocketConnectionState.Disconnected); - - Debug.WriteLine("creating subscription stream"); - IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest, errorMonitor.Invoke); - - Debug.WriteLine("subscribing..."); - var tester = observable.Monitor(); - statusMonitor.Should().HaveReceivedPayload().Which.Should() - .Be(GraphQLWebsocketConnectionState.Connecting); - statusMonitor.Should().HaveReceivedPayload().Which.Should() - .Be(GraphQLWebsocketConnectionState.Connected); - 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); - - Debug.WriteLine("stopping web host..."); - await server.StopAsync(CancellationToken.None).ConfigureAwait(false); - server.Dispose(); - Debug.WriteLine("web host stopped..."); - - errorMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)) - .Which.Should().BeOfType(); - statusMonitor.Should().HaveReceivedPayload().Which.Should() - .Be(GraphQLWebsocketConnectionState.Disconnected); - - server = CreateServer(port); - reconnectBlocker.Set(); - statusMonitor.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)).Which.Should() - .Be(GraphQLWebsocketConnectionState.Connecting); - statusMonitor.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)).Which.Should() - .Be(GraphQLWebsocketConnectionState.Connected); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - - // disposing the client should complete the subscription - client.Dispose(); - tester.Should().HaveCompleted(TimeSpan.FromSeconds(5)); - server.Dispose(); + var websocketStates = new ConcurrentQueue(); + + using (client.WebsocketConnectionState.Subscribe(websocketStates.Enqueue)) { + websocketStates.Should().ContainSingle(state => state == GraphQLWebsocketConnectionState.Disconnected); + + Debug.WriteLine("creating subscription stream"); + IObservable> observable = + client.CreateSubscriptionStream(SubscriptionRequest, + errorMonitor.Invoke); + + Debug.WriteLine("subscribing..."); + var tester = observable.Monitor(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + + websocketStates.Should().ContainInOrder( + GraphQLWebsocketConnectionState.Disconnected, + GraphQLWebsocketConnectionState.Connecting, + GraphQLWebsocketConnectionState.Connected); + // clear the collection so the next tests on the collection work as expected + websocketStates.Clear(); + + 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); + + Debug.WriteLine("stopping web host..."); + await server.StopAsync(CancellationToken.None).ConfigureAwait(false); + server.Dispose(); + Debug.WriteLine("web host stopped..."); + + errorMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)) + .Which.Should().BeOfType(); + websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); + + server = CreateServer(port); + reconnectBlocker.Set(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + websocketStates.Should().ContainInOrder( + GraphQLWebsocketConnectionState.Disconnected, + GraphQLWebsocketConnectionState.Connecting, + GraphQLWebsocketConnectionState.Connected); + + // disposing the client should complete the subscription + client.Dispose(); + tester.Should().HaveCompleted(TimeSpan.FromSeconds(5)); + server.Dispose(); + } } From 34b7d03ba2419186060176fe93fd14d130493bb9 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 15:23:56 +0100 Subject: [PATCH 46/55] test publish job on branch workflow --- .github/workflows/branches-ubuntu.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 488f6596..1f420844 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -68,3 +68,22 @@ jobs: run: jq -r 'to_entries|map("::set-env name=GitVersion_\(.key)::\(.value|tostring)")|.[]' gitversion.json - name: Run tests run: echo "Current version is \"$GitVersion_SemVer\"" && dotnet test -c Release + + publish: + name: Publish + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download nuget packages + uses: actions/download-artifact@v1 + with: + name: nupkg + - uses: actions/setup-dotnet@v1 + with: + source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json + env: + NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Publish the package to GPR + run: dotnet nuget push ./nupkg/*.nupkg -k ${{secrets.GITHUB_TOKEN}} From 76c1be9459caccc3d79163b82de14f3926c09d3d Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 15:43:28 +0100 Subject: [PATCH 47/55] fix publish job to publish all packages --- .github/workflows/branches-ubuntu.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 1f420844..931976da 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -80,10 +80,11 @@ jobs: uses: actions/download-artifact@v1 with: name: nupkg + path: ./ - uses: actions/setup-dotnet@v1 with: source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json env: NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Publish the package to GPR - run: dotnet nuget push ./nupkg/*.nupkg -k ${{secrets.GITHUB_TOKEN}} + run: dotnet nuget push *.nupkg -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate From 33b99e97541b0c26034fc2e49cbb513c893c68e8 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 16:05:48 +0100 Subject: [PATCH 48/55] use for loop to publish packages --- .github/workflows/branches-ubuntu.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 931976da..31b1d511 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -80,11 +80,10 @@ jobs: uses: actions/download-artifact@v1 with: name: nupkg - path: ./ - uses: actions/setup-dotnet@v1 with: source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json env: NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Publish the package to GPR - run: dotnet nuget push *.nupkg -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate + run: for pkg in ./nupkg/*.nupkg; do dotnet nuget push *.nupkg -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate; done From 00a4875ed4db8d62458fa06efc35f585e40bbd97 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 16:06:33 +0100 Subject: [PATCH 49/55] fix for loop --- .github/workflows/branches-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 31b1d511..c5c79ee1 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -86,4 +86,4 @@ jobs: env: NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Publish the package to GPR - run: for pkg in ./nupkg/*.nupkg; do dotnet nuget push *.nupkg -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate; done + run: for p in ./nupkg/*.nupkg; do dotnet nuget push $p -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate; done From cb59c7476f6235cbbe53ecacf1f4d6b5a55a18bc Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 16:18:03 +0100 Subject: [PATCH 50/55] enable continuous deployment mode for all branches --- GitVersion.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/GitVersion.yml b/GitVersion.yml index 4b1b821f..2210324b 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,3 +1 @@ -branches: - release: - mode: ContinuousDeployment +mode: ContinuousDeployment From d5e313cf26b0a25e957de158f479b90402aa1b5e Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 16:38:40 +0100 Subject: [PATCH 51/55] create main workflow for ubuntu --- .../{branches.yml => branches-windows.yml} | 0 .github/workflows/main-ubuntu.yml | 92 +++++++++++++++++++ .../workflows/{main.yml => main-windows.yml} | 11 +-- 3 files changed, 95 insertions(+), 8 deletions(-) rename .github/workflows/{branches.yml => branches-windows.yml} (100%) create mode 100644 .github/workflows/main-ubuntu.yml rename .github/workflows/{main.yml => main-windows.yml} (96%) diff --git a/.github/workflows/branches.yml b/.github/workflows/branches-windows.yml similarity index 100% rename from .github/workflows/branches.yml rename to .github/workflows/branches-windows.yml diff --git a/.github/workflows/main-ubuntu.yml b/.github/workflows/main-ubuntu.yml new file mode 100644 index 00000000..2cbffd4e --- /dev/null +++ b/.github/workflows/main-ubuntu.yml @@ -0,0 +1,92 @@ +name: Main workflow (Ubuntu) +on: + push: + branches: + - develop + - 'release/**' + - 'releases/**' + tags: + - v* + - V* +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true +jobs: + generateVersionInfo: + name: GenerateVersionInfo + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Restore dotnet tools + run: dotnet tool restore + - name: Fetch complete repository + run: git fetch + - name: Generate version info from git history + run: dotnet gitversion /output json > gitversion.json + - name: Upload version info file + uses: actions/upload-artifact@v1 + with: + name: gitversion + path: gitversion.json + + build: + name: Build + needs: generateVersionInfo + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download version info file + uses: actions/download-artifact@v1 + with: + name: gitversion + path: ./ + - name: Inject version info into environment + run: jq -r 'to_entries|map("::set-env name=GitVersion_\(.key)::\(.value|tostring)")|.[]' gitversion.json + - name: Build solution + run: echo "Current version is \"$GitVersion_SemVer\"" && dotnet build -c Release + - name: Create NuGet packages + run: dotnet pack -c Release --no-build -o nupkg + - name: Upload nuget packages + uses: actions/upload-artifact@v1 + with: + name: nupkg + path: nupkg + + test: + name: Test + needs: [build, generateVersionInfo] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download version info file + uses: actions/download-artifact@v1 + with: + name: gitversion + path: ./ + - name: Inject version info into environment + run: jq -r 'to_entries|map("::set-env name=GitVersion_\(.key)::\(.value|tostring)")|.[]' gitversion.json + - name: Run tests + run: echo "Current version is \"$GitVersion_SemVer\"" && dotnet test -c Release + + publish: + name: Publish + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download nuget packages + uses: actions/download-artifact@v1 + with: + name: nupkg + - uses: actions/setup-dotnet@v1 + with: + source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json + env: + NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Publish the package to GPR + run: for p in ./nupkg/*.nupkg; do dotnet nuget push $p -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate; done diff --git a/.github/workflows/main.yml b/.github/workflows/main-windows.yml similarity index 96% rename from .github/workflows/main.yml rename to .github/workflows/main-windows.yml index 2ed05127..8ab30600 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main-windows.yml @@ -1,13 +1,8 @@ name: Main workflow -on: +on: push: - branches: - - develop - - 'release/**' - - 'releases/**' - tags: - - v* - - V* + branches-ignore: + - '**' jobs: generateVersionInfo: name: GenerateVersionInfo From 35052f60d9be25146290e9c82ea78a1862d1da96 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 19 Feb 2020 16:56:22 +0100 Subject: [PATCH 52/55] cd into nupkg dir --- .github/workflows/branches-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index c5c79ee1..9ec4e076 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -86,4 +86,4 @@ jobs: env: NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Publish the package to GPR - run: for p in ./nupkg/*.nupkg; do dotnet nuget push $p -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate; done + run: cd ./nupkg && dotnet nuget push *.nupkg -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate From 3ac99ef0bae0580be59665258a41207bd4d8efc0 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Thu, 20 Feb 2020 09:46:26 +0100 Subject: [PATCH 53/55] try package upload workaround --- .github/workflows/branches-ubuntu.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 9ec4e076..747b0e35 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -86,4 +86,8 @@ jobs: env: NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Publish the package to GPR - run: cd ./nupkg && dotnet nuget push *.nupkg -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate + run: | + for f in ./nupkg/*.nupkg + do + curl -vX PUT -u "graphql-dotnet:${{ secrets.GHPackagesToken }}" -F package=@$f https://nuget.pkg.github.com/graphql-dotnet/ + done From 94f9cb7a151aca5ff9226cc7ae9eb70b9a09bd06 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Thu, 20 Feb 2020 09:47:14 +0100 Subject: [PATCH 54/55] fix secret --- .github/workflows/branches-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 747b0e35..059ca0c9 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -89,5 +89,5 @@ jobs: run: | for f in ./nupkg/*.nupkg do - curl -vX PUT -u "graphql-dotnet:${{ secrets.GHPackagesToken }}" -F package=@$f https://nuget.pkg.github.com/graphql-dotnet/ + curl -vX PUT -u "graphql-dotnet:${{secrets.GITHUB_TOKEN}}" -F package=@$f https://nuget.pkg.github.com/graphql-dotnet/ done From 43e8201ee3a1c17ed157ace229980eda89664b1f Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Thu, 20 Feb 2020 09:54:06 +0100 Subject: [PATCH 55/55] move publish step to main workflow --- .github/workflows/branches-ubuntu.yml | 23 ----------------------- .github/workflows/main-ubuntu.yml | 14 +++++++------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/.github/workflows/branches-ubuntu.yml b/.github/workflows/branches-ubuntu.yml index 059ca0c9..488f6596 100644 --- a/.github/workflows/branches-ubuntu.yml +++ b/.github/workflows/branches-ubuntu.yml @@ -68,26 +68,3 @@ jobs: run: jq -r 'to_entries|map("::set-env name=GitVersion_\(.key)::\(.value|tostring)")|.[]' gitversion.json - name: Run tests run: echo "Current version is \"$GitVersion_SemVer\"" && dotnet test -c Release - - publish: - name: Publish - needs: [test] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Download nuget packages - uses: actions/download-artifact@v1 - with: - name: nupkg - - uses: actions/setup-dotnet@v1 - with: - source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json - env: - NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - name: Publish the package to GPR - run: | - for f in ./nupkg/*.nupkg - do - curl -vX PUT -u "graphql-dotnet:${{secrets.GITHUB_TOKEN}}" -F package=@$f https://nuget.pkg.github.com/graphql-dotnet/ - done diff --git a/.github/workflows/main-ubuntu.yml b/.github/workflows/main-ubuntu.yml index 2cbffd4e..bb58dcff 100644 --- a/.github/workflows/main-ubuntu.yml +++ b/.github/workflows/main-ubuntu.yml @@ -83,10 +83,10 @@ jobs: uses: actions/download-artifact@v1 with: name: nupkg - - uses: actions/setup-dotnet@v1 - with: - source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json - env: - NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - name: Publish the package to GPR - run: for p in ./nupkg/*.nupkg; do dotnet nuget push $p -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate; done + - name: Publish the package to GPR + # using workaround with CURL because of non-functioning upload via dotnet nuget (https://stackoverflow.com/a/58943251) + run: | + for f in ./nupkg/*.nupkg + do + curl -vX PUT -u "graphql-dotnet:${{secrets.GITHUB_TOKEN}}" -F package=@$f https://nuget.pkg.github.com/graphql-dotnet/ + done