From 76369444fac3cf9f6039e2454ae1525e072a8cb5 Mon Sep 17 00:00:00 2001 From: "Steve G. Bjorg" Date: Tue, 8 Sep 2020 12:22:53 -0700 Subject: [PATCH 1/6] allow wss:// scheme, preserve uri query parameters --- src/GraphQL.Client/GraphQLHttpClient.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index ea828ec1..9b3b86ae 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -66,6 +66,10 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson HttpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(GetType().Assembly.GetName().Name, GetType().Assembly.GetName().Version.ToString())); _lazyHttpWebSocket = new Lazy(() => new GraphQLHttpWebSocket(GetWebSocketUri(), this)); + if ((Options.EndPoint?.Scheme == "wss") || (Options.EndPoint?.Scheme == "ws")) + { + Options.UseWebSocketForQueriesAndMutations = true; + } } #endregion @@ -155,8 +159,12 @@ private async Task> SendHttpRequestAsync Date: Tue, 8 Sep 2020 15:59:03 -0700 Subject: [PATCH 2/6] added option for explicit WebSocket endpoint --- src/GraphQL.Client/GraphQLHttpClient.cs | 2 ++ src/GraphQL.Client/GraphQLHttpClientOptions.cs | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 9b3b86ae..09c352ee 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -159,6 +159,8 @@ private async Task> SendHttpRequestAsync - /// The GraphQL EndPoint to be used + /// The GraphQL EndPoint to be used for HTTP connections /// public Uri EndPoint { get; set; } + /// + /// The GraphQL EndPoint to be used for WebSocket connection + /// + public Uri WebSocketEndPoint { get; set; } + /// /// The that is going to be used /// From 838e9b228b7c2e0cb434d47031e95ddfc6b91a30 Mon Sep 17 00:00:00 2001 From: "Steve G. Bjorg" Date: Tue, 8 Sep 2020 15:59:59 -0700 Subject: [PATCH 3/6] make Query property optional --- src/GraphQL.Primitives/GraphQLRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GraphQL.Primitives/GraphQLRequest.cs b/src/GraphQL.Primitives/GraphQLRequest.cs index 72033552..f83bb0e6 100644 --- a/src/GraphQL.Primitives/GraphQLRequest.cs +++ b/src/GraphQL.Primitives/GraphQLRequest.cs @@ -86,7 +86,7 @@ public override int GetHashCode() { unchecked { - var hashCode = Query.GetHashCode(); + var hashCode = Query?.GetHashCode() ?? 0; hashCode = (hashCode * 397) ^ OperationName?.GetHashCode() ?? 0; hashCode = (hashCode * 397) ^ Variables?.GetHashCode() ?? 0; return hashCode; From 36b3ef03e368e69465f76061d81eb12cfa69f0ae Mon Sep 17 00:00:00 2001 From: "Steve G. Bjorg" Date: Wed, 9 Sep 2020 16:27:13 -0700 Subject: [PATCH 4/6] debugging support --- .../Websocket/GraphQLHttpWebSocket.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index fec76d34..1c78ebc4 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -135,6 +135,7 @@ public IObservable> CreateSubscriptionStream( response.MessageBytes); + Debug.WriteLine($"payload => {System.Text.Encoding.UTF8.GetString(response.MessageBytes)}"); o.OnNext(typedResponse.Payload); // in case of a GraphQL error, terminate the sequence after the response has been posted @@ -193,10 +194,12 @@ public IObservable> CreateSubscriptionStream> CreateSubscriptionStream> CreateSubscriptionStream { - Debug.WriteLine($"unwrap exception thread id: {Thread.CurrentThread.ManagedThreadId}"); // if the result contains an exception, throw it on the observable if (t.Item2 != null) + { + Debug.WriteLine($"unwrap exception thread id: {Thread.CurrentThread.ManagedThreadId} => {t.Item2}"); return Observable.Throw>(t.Item2); - - return t.Item1 == null - ? Observable.Empty>() - : Observable.Return(t.Item1); + } + if (t.Item1 == null) + { + Debug.WriteLine($"empty item thread id: {Thread.CurrentThread.ManagedThreadId}"); + return Observable.Empty>(); + } + return Observable.Return(t.Item1); }) // transform to hot observable and auto-connect .Publish().RefCount(); @@ -318,7 +325,7 @@ public Task> SendRequest(GraphQLRequest re } catch (Exception e) { - Console.WriteLine(e); + Debug.WriteLine(e); throw; } From 5a3f2336cf190c333871333a0ecdf77782454454 Mon Sep 17 00:00:00 2001 From: "Steve G. Bjorg" Date: Thu, 17 Sep 2020 17:36:30 -0700 Subject: [PATCH 5/6] merge master --- README.md | 6 + .../GraphQL.Client.Abstractions.csproj | 4 + .../Utilities/StringExtensions.cs | 37 ++++ .../Utilities/StringUtils.cs | 193 ++++++++++++++++++ .../GraphQL.Client.LocalExecution.csproj | 1 + .../GraphQLLocalExecutionClient.cs | 7 +- .../ConstantCaseEnumConverter.cs} | 8 +- .../NewtonsoftJsonSerializer.cs | 3 +- .../ConstantCaseJsonNamingPolicy.cs | 10 + .../SystemTextJsonSerializer.cs | 4 +- src/GraphQL.Client/GraphQLHttpClient.cs | 21 +- src/GraphQL.Client/UriExtensions.cs | 36 ++++ .../Websocket/GraphQLHttpWebSocket.cs | 44 ++-- .../NewtonsoftSerializerTest.cs | 2 +- .../SystemTextJsonSerializerTests.cs | 3 +- .../TestData/SerializeToStringTestData.cs | 16 ++ .../UriExtensionTests.cs | 47 +++++ .../WebsocketTests/Base.cs | 10 + 18 files changed, 399 insertions(+), 53 deletions(-) create mode 100644 src/GraphQL.Client.Abstractions/Utilities/StringExtensions.cs create mode 100644 src/GraphQL.Client.Abstractions/Utilities/StringUtils.cs rename src/{GraphQL.Client.LocalExecution/GraphQLEnumConverter.cs => GraphQL.Client.Serializer.Newtonsoft/ConstantCaseEnumConverter.cs} (81%) create mode 100644 src/GraphQL.Client.Serializer.SystemTextJson/ConstantCaseJsonNamingPolicy.cs create mode 100644 src/GraphQL.Client/UriExtensions.cs create mode 100644 tests/GraphQL.Integration.Tests/UriExtensionTests.cs diff --git a/README.md b/README.md index 274e92e4..32d35da0 100644 --- a/README.md +++ b/README.md @@ -139,3 +139,9 @@ subscription.Dispose(); * [GitHub GraphQL API Docs](https://developer.github.com/v4/guides/forming-calls/) * [GitHub GraphQL Explorer](https://developer.github.com/v4/explorer/) * [GitHub GraphQL Endpoint](https://api.github.com/graphql) + +## Blazor WebAssembly Limitations + +Blazor WebAssembly differs from other platforms as it does not support all features of other .NET runtime implementations. For instance, the following WebSocket options properties are not supported and will not be set: +* [ClientCertificates](https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions.clientcertificates?view=netcore-3.1#System_Net_WebSockets_ClientWebSocketOptions_ClientCertificates) +* [UseDefaultCredentials](https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions.usedefaultcredentials?view=netcore-3.1) diff --git a/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj b/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj index d7678b66..8e1faee1 100644 --- a/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj +++ b/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj @@ -15,4 +15,8 @@ + + + + diff --git a/src/GraphQL.Client.Abstractions/Utilities/StringExtensions.cs b/src/GraphQL.Client.Abstractions/Utilities/StringExtensions.cs new file mode 100644 index 00000000..38c718a8 --- /dev/null +++ b/src/GraphQL.Client.Abstractions/Utilities/StringExtensions.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace GraphQL.Client.Abstractions.Utilities +{ + /// + /// Copied from https://github.com/jquense/StringUtils + /// + public static class StringExtensions + { + public static string StripIndent(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.StripIndent(str); + + public static IEnumerable ToWords(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToWords(str); + + public static string ToUpperFirst(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToUpperFirst(str); + + public static string ToLowerFirst(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToLowerFirst(str); + + public static string Capitalize(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.Capitalize(str); + + public static string ToCamelCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToCamelCase(str); + + public static string ToConstantCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToConstantCase(str); + + public static string ToUpperCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToUpperCase(str); + + public static string ToLowerCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToLowerCase(str); + + + public static string ToPascalCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToPascalCase(str); + + + public static string ToKebabCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToKebabCase(str); + + + public static string ToSnakeCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToSnakeCase(str); + } +} diff --git a/src/GraphQL.Client.Abstractions/Utilities/StringUtils.cs b/src/GraphQL.Client.Abstractions/Utilities/StringUtils.cs new file mode 100644 index 00000000..c643fd23 --- /dev/null +++ b/src/GraphQL.Client.Abstractions/Utilities/StringUtils.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace GraphQL.Client.Abstractions.Utilities +{ + /// + /// Copied from https://github.com/jquense/StringUtils + /// + public static class StringUtils + { + private static readonly Regex _reWords = new Regex(@"[A-Z\xc0-\xd6\xd8-\xde]?[a-z\xdf-\xf6\xf8-\xff]+(?:['’](?:d|ll|m|re|s|t|ve))?(?=[\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000]|[A-Z\xc0-\xd6\xd8-\xde]|$)|(?:[A-Z\xc0-\xd6\xd8-\xde]|[^\ud800-\udfff\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\d+\u2700-\u27bfa-z\xdf-\xf6\xf8-\xffA-Z\xc0-\xd6\xd8-\xde])+(?:['’](?:D|LL|M|RE|S|T|VE))?(?=[\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000]|[A-Z\xc0-\xd6\xd8-\xde](?:[a-z\xdf-\xf6\xf8-\xff]|[^\ud800-\udfff\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\d+\u2700-\u27bfa-z\xdf-\xf6\xf8-\xffA-Z\xc0-\xd6\xd8-\xde])|$)|[A-Z\xc0-\xd6\xd8-\xde]?(?:[a-z\xdf-\xf6\xf8-\xff]|[^\ud800-\udfff\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\d+\u2700-\u27bfa-z\xdf-\xf6\xf8-\xffA-Z\xc0-\xd6\xd8-\xde])+(?:['’](?:d|ll|m|re|s|t|ve))?|[A-Z\xc0-\xd6\xd8-\xde]+(?:['’](?:D|LL|M|RE|S|T|VE))?|\d+|(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*"); + private static readonly Regex _reIndent = new Regex(@"^[ \t]*(?=\S)", RegexOptions.Multiline); + + /// + /// Removes the leading indent from a multi-line string + /// + /// String + /// + public static string StripIndent(string str) + { + int indent = _reIndent.Matches(str).Cast().Select(m => m.Value.Length).Min(); + return new Regex(@"^[ \t]{" + indent + "}", RegexOptions.Multiline).Replace(str, ""); + } + + /// + /// Split a cased string into a series of "words" excluding the seperator. + /// + /// + /// + public static IEnumerable ToWords(string str) + { + foreach (Match match in _reWords.Matches(str)) + { + yield return match.Value; + } + } + + /// + /// Uppercase the first character in a string, leaving the rest of the string as is + /// + /// + /// a string with the first character uppercased + public static string ToUpperFirst(string str) => ChangeCaseFirst(str, c => c.ToUpperInvariant()); + + /// + /// Lowercase the first character in a string, leaving the rest of the string as is + /// + /// + /// a string with the first character lowercased + public static string ToLowerFirst(string str) => ChangeCaseFirst(str, c => c.ToLowerInvariant()); + + /// + /// Capitalizes a string, lowercasing the entire string and uppercasing the first character + /// + /// + /// a capitalized string + public static string Capitalize(string str) => ToUpperFirst(str.ToLowerInvariant()); + + /// + /// Converts a string to camelCase. + /// + /// + /// StringUtils.ToCamelCase("FOOBAR") // "foobar" + /// StringUtils.ToCamelCase("FOO_BAR") // "fooBar" + /// StringUtils.ToCamelCase("FooBar") // "fooBar" + /// StringUtils.ToCamelCase("foo bar") // "fooBar" + /// + /// + /// + public static string ToCamelCase(string str) => + ChangeCase(str, (word, index) => + (index == 0 ? word.ToLowerInvariant() : Capitalize(word))); + + /// + /// Convert a string to CONSTANT_CASE + /// + /// + /// StringUtils.ToConstantCase("fOo BaR") // "FOO_BAR" + /// StringUtils.ToConstantCase("FooBar") // "FOO_BAR" + /// StringUtils.ToConstantCase("Foo Bar") // "FOO_BAR" + /// + /// + /// + public static string ToConstantCase(string str) => ChangeCase(str, "_", w => w.ToUpperInvariant()); + + /// + /// Convert a string to UPPERCASE + /// + /// + /// StringUtils.ToUpperCase("foobar") // "FOOBAR" + /// StringUtils.ToUpperCase("FOO_BAR") // "FOO BAR" + /// StringUtils.ToUpperCase("FooBar") // "FOO BAR" + /// StringUtils.ToUpperCase("Foo Bar") // "FOO BAR" + /// + /// + /// + public static string ToUpperCase(string str) => ChangeCase(str, " ", (word) => word.ToUpperInvariant()); + + /// + /// Convert a string to lowercase + /// + /// + /// StringUtils.ToLowerCase("FOOBAR") // "foobar" + /// StringUtils.ToLowerCase("FOO_BAR") // "foo bar" + /// StringUtils.ToLowerCase("FooBar") // "foo bar" + /// StringUtils.ToLowerCase("Foo Bar") // "foo bar" + /// + /// + /// + public static string ToLowerCase(string str) => ChangeCase(str, " ", word => word.ToLowerInvariant()); + + /// + /// convert a string to PascalCase + /// + /// + /// StringUtils.ToPascalCase("FOOBAR") // "FooBar" + /// StringUtils.ToPascalCase("FOO_BAR") // "FooBar" + /// StringUtils.ToPascalCase("fooBar") // "FooBar" + /// StringUtils.ToPascalCase("Foo Bar") // "FooBar" + /// + /// + /// + public static string ToPascalCase(string str) => ChangeCase(str, Capitalize); + + /// + /// convert a string to kebab-case + /// + /// + /// StringUtils.ToKebabCase("FOOBAR") // "foo-bar" + /// StringUtils.ToKebabCase("FOO_BAR") // "foo-bar" + /// StringUtils.ToKebabCase("fooBar") // "foo-bar" + /// StringUtils.ToKebabCase("Foo Bar") // "foo-bar" + /// + /// + /// + public static string ToKebabCase(string str) => ChangeCase(str, "-", word => word.ToLowerInvariant()); + + /// + /// convert a string to snake_case + /// + /// + /// StringUtils.ToSnakeCase("FOOBAR") // "foo_bar" + /// StringUtils.ToSnakeCase("FOO_BAR") // "foo_bar" + /// StringUtils.ToSnakeCase("fooBar") // "foo_bar" + /// StringUtils.ToSnakeCase("Foo Bar") // "foo_bar" + /// + /// + /// + public static string ToSnakeCase(string str) => ChangeCase(str, "_", word => word.ToLowerInvariant()); + + public static string ChangeCase(string str, Func composer) => ChangeCase(str, "", composer); + + public static string ChangeCase(string str, string sep, Func composer) => ChangeCase(str, sep, (w, i) => composer(w)); + + public static string ChangeCase(string str, Func composer) => ChangeCase(str, "", composer); + + /// + /// Convert a string to a new case + /// + /// + /// Convert a string to inverse camelCase: CAMELcASE + /// + /// StringUtils.ChangeCase("my string", "", (word, index) => { + /// word = word.ToUpperInvariant(); + /// if (index > 0) + /// word = StringUtils.toLowerFirst(word); + /// return word + /// }); + /// // "MYsTRING" + /// + /// + /// an input string + /// a seperator string used between "words" in the string + /// a function that converts individual words to a new case + /// + public static string ChangeCase(string str, string sep, Func composer) + { + string result = ""; + int index = 0; + + foreach (string word in ToWords(str)) + { + result += ((index == 0 ? "" : sep) + composer(word, index++)); + } + + return result; + } + + private static string ChangeCaseFirst(string str, Func change) => change(str.Substring(0, 1)) + str.Substring(1); + } +} diff --git a/src/GraphQL.Client.LocalExecution/GraphQL.Client.LocalExecution.csproj b/src/GraphQL.Client.LocalExecution/GraphQL.Client.LocalExecution.csproj index ebef7ea0..e8e162cb 100644 --- a/src/GraphQL.Client.LocalExecution/GraphQL.Client.LocalExecution.csproj +++ b/src/GraphQL.Client.LocalExecution/GraphQL.Client.LocalExecution.csproj @@ -15,6 +15,7 @@ + diff --git a/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs index f8fe6503..65098ae8 100644 --- a/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs +++ b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using GraphQL.Client.Abstractions; +using GraphQL.Client.Serializer.Newtonsoft; using GraphQL.Subscription; using GraphQL.Types; using Newtonsoft.Json; @@ -31,7 +32,7 @@ public class GraphQLLocalExecutionClient : IGraphQLClient where TSchema ContractResolver = new CamelCasePropertyNamesContractResolver(), Converters = new List { - new GraphQLEnumConverter() + new ConstantCaseEnumConverter() } }; @@ -50,7 +51,7 @@ public GraphQLLocalExecutionClient(TSchema schema, IGraphQLJsonSerializer serial Schema.Initialize(); _documentExecuter = new DocumentExecuter(); } - + public void Dispose() { } public Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) @@ -80,7 +81,7 @@ private async Task>> ExecuteSubscriptionA { var result = await ExecuteAsync(request, cancellationToken); var stream = ((SubscriptionExecutionResult)result).Streams?.Values.SingleOrDefault(); - + return stream == null ? Observable.Throw>(new InvalidOperationException("the GraphQL execution did not return an observable")) : stream.SelectMany(executionResult => Observable.FromAsync(token => ExecutionResultToGraphQLResponse(executionResult, token))); diff --git a/src/GraphQL.Client.LocalExecution/GraphQLEnumConverter.cs b/src/GraphQL.Client.Serializer.Newtonsoft/ConstantCaseEnumConverter.cs similarity index 81% rename from src/GraphQL.Client.LocalExecution/GraphQLEnumConverter.cs rename to src/GraphQL.Client.Serializer.Newtonsoft/ConstantCaseEnumConverter.cs index a02cf935..d618a490 100644 --- a/src/GraphQL.Client.LocalExecution/GraphQLEnumConverter.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/ConstantCaseEnumConverter.cs @@ -1,13 +1,13 @@ using System; using System.Linq; using System.Reflection; -using GraphQL.Utilities; +using GraphQL.Client.Abstractions.Utilities; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace GraphQL.Client.LocalExecution +namespace GraphQL.Client.Serializer.Newtonsoft { - public class GraphQLEnumConverter : StringEnumConverter + public class ConstantCaseEnumConverter : StringEnumConverter { public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { @@ -29,7 +29,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } else { - writer.WriteValue(StringUtils.ToConstantCase(memberName)); + writer.WriteValue(memberName.ToConstantCase()); } } } diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index 5ec06c1a..e7cae90f 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -15,7 +15,8 @@ public class NewtonsoftJsonSerializer : IGraphQLWebsocketJsonSerializer public static JsonSerializerSettings DefaultJsonSerializerSettings => new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver { IgnoreIsSpecifiedMembers = true }, - MissingMemberHandling = MissingMemberHandling.Ignore + MissingMemberHandling = MissingMemberHandling.Ignore, + Converters = { new ConstantCaseEnumConverter() } }; public JsonSerializerSettings JsonSerializerSettings { get; } diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/ConstantCaseJsonNamingPolicy.cs b/src/GraphQL.Client.Serializer.SystemTextJson/ConstantCaseJsonNamingPolicy.cs new file mode 100644 index 00000000..138b0276 --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/ConstantCaseJsonNamingPolicy.cs @@ -0,0 +1,10 @@ +using System.Text.Json; +using GraphQL.Client.Abstractions.Utilities; + +namespace GraphQL.Client.Serializer.SystemTextJson +{ + public class ConstantCaseJsonNamingPolicy: JsonNamingPolicy + { + public override string ConvertName(string name) => name.ToConstantCase(); + } +} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index a1667ec8..d0325464 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using GraphQL.Client.Abstractions; @@ -12,7 +13,8 @@ public class SystemTextJsonSerializer : IGraphQLWebsocketJsonSerializer { public static JsonSerializerOptions DefaultJsonSerializerOptions => new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter(new ConstantCaseJsonNamingPolicy(), false)} }.SetupImmutableConverter(); public JsonSerializerOptions Options { get; } diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 09c352ee..77db02d3 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -65,11 +65,7 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson if (!HttpClient.DefaultRequestHeaders.UserAgent.Any()) HttpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(GetType().Assembly.GetName().Name, GetType().Assembly.GetName().Version.ToString())); - _lazyHttpWebSocket = new Lazy(() => new GraphQLHttpWebSocket(GetWebSocketUri(), this)); - if ((Options.EndPoint?.Scheme == "wss") || (Options.EndPoint?.Scheme == "ws")) - { - Options.UseWebSocketForQueriesAndMutations = true; - } + _lazyHttpWebSocket = new Lazy(() => new GraphQLHttpWebSocket(Options.EndPoint.GetWebSocketUri(), this)); } #endregion @@ -79,7 +75,7 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson /// public async Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { - if (Options.UseWebSocketForQueriesAndMutations) + if (Options.UseWebSocketForQueriesAndMutations || Options.EndPoint.HasWebSocketScheme()) return await _graphQlHttpWebSocket.SendRequest(request, cancellationToken); return await SendHttpRequestAsync(request, cancellationToken); @@ -156,19 +152,6 @@ private async Task> SendHttpRequestAsync + /// Returns true if equals "wss" or "ws" + /// + /// + /// + public static bool HasWebSocketScheme(this Uri uri) => uri.Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) || uri.Scheme.Equals("ws", StringComparison.OrdinalIgnoreCase); + + /// + /// Infers the websocket uri from . + /// + /// + /// + public static Uri GetWebSocketUri(this Uri uri) + { + if (uri.HasWebSocketScheme()) + return uri; + + string webSocketScheme; + + if (uri.Scheme == Uri.UriSchemeHttps) + webSocketScheme = "wss"; + else if (uri.Scheme == Uri.UriSchemeHttp) + webSocketScheme = "ws"; + else + throw new NotSupportedException($"cannot infer websocket uri from uri scheme {uri.Scheme}"); + + return new UriBuilder(uri){Scheme = webSocketScheme}.Uri; + } + } +} diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 1c78ebc4..e6ee8434 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -114,6 +114,7 @@ public IObservable> CreateSubscriptionStream>(o => @@ -414,22 +415,12 @@ public Task InitializeWebSocket() #else _clientWebSocket = new ClientWebSocket(); _clientWebSocket.Options.AddSubProtocol("graphql-ws"); - try + if(!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Create("WEBASSEMBLY"))) { + // the following properties are not supported in Blazor WebAssembly and throw a PlatformNotSupportedException error when accessed _clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; - } - catch (PlatformNotSupportedException) - { - Debug.WriteLine("unable to set Options.ClientCertificates property; platform does not support it"); - } - try - { _clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; } - catch (PlatformNotSupportedException) - { - Debug.WriteLine("unable to set Options.UseDefaultCredentials property; platform does not support it"); - } Options.ConfigureWebsocketOptions(_clientWebSocket.Options); #endif return _initializeWebSocketTask = ConnectAsync(_internalCancellationToken); @@ -570,16 +561,23 @@ private async Task ReceiveWebsocketMessagesAsync() _internalCancellationToken.ThrowIfCancellationRequested(); ms.Seek(0, SeekOrigin.Begin); - if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) + switch (webSocketReceiveResult.MessageType) { - var response = await _client.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); - response.MessageBytes = ms.ToArray(); - Debug.WriteLine($"{response.MessageBytes.Length} bytes received for id {response.Id} on websocket {_clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); - return response; - } - else - { - throw new NotSupportedException("binary websocket messages are not supported"); + case WebSocketMessageType.Text: + var response = await _client.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); + response.MessageBytes = ms.ToArray(); + Debug.WriteLine($"{response.MessageBytes.Length} bytes received for id {response.Id} on websocket {_clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); + return response; + + case WebSocketMessageType.Close: + var closeResponse = await _client.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); + closeResponse.MessageBytes = ms.ToArray(); + Debug.WriteLine($"Connection closed by the server."); + throw new Exception("Connection closed by the server."); + + default: + throw new NotSupportedException($"Websocket message type {webSocketReceiveResult.MessageType} not supported."); + } } catch (Exception e) @@ -626,7 +624,7 @@ public void Complete() /// /// 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; } @@ -650,7 +648,7 @@ private async Task CompleteAsync() _exceptionSubject?.OnCompleted(); _exceptionSubject?.Dispose(); _internalCancellationTokenSource.Dispose(); - + Debug.WriteLine("GraphQLHttpWebSocket disposed"); } diff --git a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs index 00b6d03b..43ade957 100644 --- a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs @@ -11,6 +11,6 @@ public NewtonsoftSerializerTest() : base(new NewtonsoftJsonSerializer()) { } public class NewtonsoftSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest { public NewtonsoftSerializeNoCamelCaseTest() - : base(new NewtonsoftJsonSerializer(new JsonSerializerSettings())) { } + : base(new NewtonsoftJsonSerializer(new JsonSerializerSettings(){ Converters = { new ConstantCaseEnumConverter() } })) { } } } diff --git a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs index 217c9da1..153bcd4d 100644 --- a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs +++ b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Serialization; using GraphQL.Client.Serializer.SystemTextJson; namespace GraphQL.Client.Serializer.Tests @@ -11,6 +12,6 @@ public SystemTextJsonSerializerTests() : base(new SystemTextJsonSerializer()) { public class SystemTextJsonSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest { public SystemTextJsonSerializeNoCamelCaseTest() - : base(new SystemTextJsonSerializer(new JsonSerializerOptions().SetupImmutableConverter())) { } + : base(new SystemTextJsonSerializer(new JsonSerializerOptions(){Converters = { new JsonStringEnumConverter(new ConstantCaseJsonNamingPolicy(), false)}}.SetupImmutableConverter())) { } } } diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs index 06208c21..e5285681 100644 --- a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs @@ -1,5 +1,7 @@ +using System; using System.Collections; using System.Collections.Generic; +using System.Linq; namespace GraphQL.Client.Serializer.Tests.TestData { @@ -19,8 +21,22 @@ public IEnumerator GetEnumerator() "{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null,\"authentication\":\"an-authentication-token\"}", new GraphQLRequest("simple query string"){{"authentication", "an-authentication-token"}} }; + yield return new object[] { + "{\"query\":\"enumtest\",\"variables\":{\"enums\":[\"REGULAR\",\"PASCAL_CASE\",\"CAMEL_CASE\",\"LOWER\",\"UPPER\",\"CONSTANT_CASE\"]},\"operationName\":null}", + new GraphQLRequest("enumtest", new { enums = Enum.GetValues(typeof(TestEnum)).Cast()}) + }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public enum TestEnum + { + Regular, + PascalCase, + camelCase, + lower, + UPPER, + CONSTANT_CASE + } } } diff --git a/tests/GraphQL.Integration.Tests/UriExtensionTests.cs b/tests/GraphQL.Integration.Tests/UriExtensionTests.cs new file mode 100644 index 00000000..1298ca11 --- /dev/null +++ b/tests/GraphQL.Integration.Tests/UriExtensionTests.cs @@ -0,0 +1,47 @@ +using System; +using FluentAssertions; +using GraphQL.Client.Http; +using Xunit; + +namespace GraphQL.Integration.Tests +{ + public class UriExtensionTests + { + [Theory] + [InlineData("http://thats-not-a-websocket-url.net", false)] + [InlineData("https://thats-not-a-websocket-url.net", false)] + [InlineData("ftp://thats-not-a-websocket-url.net", false)] + [InlineData("ws://that-is-a-websocket-url.net", true)] + [InlineData("wss://that-is-a-websocket-url.net", true)] + [InlineData("WS://that-is-a-websocket-url.net", true)] + [InlineData("WSS://that-is-a-websocket-url.net", true)] + public void HasWebSocketSchemaTest(string url, bool result) + { + new Uri(url).HasWebSocketScheme().Should().Be(result); + } + + [Theory] + [InlineData("http://this-url-can-be-converted.net", true, "ws://this-url-can-be-converted.net")] + [InlineData("https://this-url-can-be-converted.net", true, "wss://this-url-can-be-converted.net")] + [InlineData("HTTP://this-url-can-be-converted.net", true, "ws://this-url-can-be-converted.net")] + [InlineData("HTTPS://this-url-can-be-converted.net", true, "wss://this-url-can-be-converted.net")] + [InlineData("ws://this-url-can-be-converted.net", true, "ws://this-url-can-be-converted.net")] + [InlineData("wss://this-url-can-be-converted.net", true, "wss://this-url-can-be-converted.net")] + [InlineData("https://this-url-can-be-converted.net/and/all/elements/?are#preserved", true, "wss://this-url-can-be-converted.net/and/all/elements/?are#preserved")] + [InlineData("ftp://this-url-cannot-be-converted.net", false, null)] + // AppSync example + [InlineData("wss://example1234567890000.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=123456789ABCDEF&payload=e30=", true, "wss://example1234567890000.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=123456789ABCDEF&payload=e30=")] + public void GetWebSocketUriTest(string input, bool canConvert, string result) + { + var inputUri = new Uri(input); + if (canConvert) + { + inputUri.GetWebSocketUri().Should().BeEquivalentTo(new Uri(result)); + } + else + { + inputUri.Invoking(uri => uri.GetWebSocketUri()).Should().Throw(); + } + } + } +} diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 51857ea2..4f90c1a5 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -70,6 +70,16 @@ public async void CanSendRequestViaWebsocket() response.Data.AddMessage.Content.Should().Be(message); } + [Fact] + public async void CanUseWebSocketScheme() + { + ChatClient.Options.EndPoint = ChatClient.Options.EndPoint.GetWebSocketUri(); + await ChatClient.InitializeWebsocketConnection(); + const string message = "some random testing message"; + var response = await ChatClient.AddMessageAsync(message); + response.Data.AddMessage.Content.Should().Be(message); + } + [Fact] public async void WebsocketRequestCanBeCancelled() { From c279407e75c191e54669d2465e3796adc73de510 Mon Sep 17 00:00:00 2001 From: Steve Bjorg Date: Tue, 29 Sep 2020 15:21:45 -0700 Subject: [PATCH 6/6] appsync support --- src/GraphQL.Client/GraphQLHttpClient.cs | 2 +- src/GraphQL.Client/GraphQLHttpClientOptions.cs | 5 +++++ src/GraphQL.Client/GraphQLHttpRequest.cs | 2 +- src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs | 5 ++--- src/GraphQL.Primitives/GraphQLRequest.cs | 8 ++++++++ 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 21485f01..eb8835f5 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -65,7 +65,7 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson if (!HttpClient.DefaultRequestHeaders.UserAgent.Any()) HttpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(GetType().Assembly.GetName().Name, GetType().Assembly.GetName().Version.ToString())); - _lazyHttpWebSocket = new Lazy(() => new GraphQLHttpWebSocket(Options.EndPoint.GetWebSocketUri(), this)); + _lazyHttpWebSocket = new Lazy(() => new GraphQLHttpWebSocket(Options.WebSocketEndPoint ?? Options.EndPoint.GetWebSocketUri(), this)); } #endregion diff --git a/src/GraphQL.Client/GraphQLHttpClientOptions.cs b/src/GraphQL.Client/GraphQLHttpClientOptions.cs index 7693a7b2..c727a661 100644 --- a/src/GraphQL.Client/GraphQLHttpClientOptions.cs +++ b/src/GraphQL.Client/GraphQLHttpClientOptions.cs @@ -52,6 +52,11 @@ public class GraphQLHttpClientOptions public Func> PreprocessRequest { get; set; } = (request, client) => Task.FromResult(request is GraphQLHttpRequest graphQLHttpRequest ? graphQLHttpRequest : new GraphQLHttpRequest(request)); + /// + /// Subscription request preprocessing function. Can be used i.e. to inject authorization info into a GraphQL subscription request payload. + /// + public Func> PreprocessSubscriptionRequest { get; set; } + /// /// This callback is called after successfully establishing a websocket connection but before any regular request is made. /// diff --git a/src/GraphQL.Client/GraphQLHttpRequest.cs b/src/GraphQL.Client/GraphQLHttpRequest.cs index 00f90d4a..eb3aac14 100644 --- a/src/GraphQL.Client/GraphQLHttpRequest.cs +++ b/src/GraphQL.Client/GraphQLHttpRequest.cs @@ -16,7 +16,7 @@ public GraphQLHttpRequest(string query, object? variables = null, string? operat { } - public GraphQLHttpRequest(GraphQLRequest other): base(other.Query, other.Variables, other.OperationName) + public GraphQLHttpRequest(GraphQLRequest other): base(other) { } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index e6ee8434..488db435 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -98,12 +98,11 @@ public IObservable> CreateSubscriptionStream>(async observer => { Debug.WriteLine($"Create observable thread id: {Thread.CurrentThread.ManagedThreadId}"); - await _client.Options.PreprocessRequest(request, _client); var startRequest = new GraphQLWebSocketRequest { Id = Guid.NewGuid().ToString("N"), Type = GraphQLWebSocketMessageType.GQL_START, - Payload = request + Payload = await (_client.Options.PreprocessSubscriptionRequest?.Invoke(request, _client) ?? _client.Options.PreprocessRequest(request, _client)) }; var closeRequest = new GraphQLWebSocketRequest { @@ -415,7 +414,7 @@ public Task InitializeWebSocket() #else _clientWebSocket = new ClientWebSocket(); _clientWebSocket.Options.AddSubProtocol("graphql-ws"); - if(!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Create("WEBASSEMBLY"))) + if(!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Create("BROWSER"))) { // the following properties are not supported in Blazor WebAssembly and throw a PlatformNotSupportedException error when accessed _clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; diff --git a/src/GraphQL.Primitives/GraphQLRequest.cs b/src/GraphQL.Primitives/GraphQLRequest.cs index f83bb0e6..19c7fded 100644 --- a/src/GraphQL.Primitives/GraphQLRequest.cs +++ b/src/GraphQL.Primitives/GraphQLRequest.cs @@ -49,6 +49,14 @@ public GraphQLRequest(string query, object? variables = null, string? operationN OperationName = operationName; } + public GraphQLRequest(IEnumerable> values) + { + foreach(var kv in values) + { + Add(kv.Key, kv.Value); + } + } + /// /// Returns a value that indicates whether this instance is equal to a specified object ///