diff --git a/src/GraphQL.Client.Http/GraphQLHttpClient.cs b/src/GraphQL.Client.Http/GraphQLHttpClient.cs index b9064fd6..9a55c77e 100644 --- a/src/GraphQL.Client.Http/GraphQLHttpClient.cs +++ b/src/GraphQL.Client.Http/GraphQLHttpClient.cs @@ -115,7 +115,7 @@ private async Task> SendHttpPostRequestAsync /// The that is going to be used /// - public JsonSerializerOptions JsonSerializerOptions { get; set; } = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }.SetupExtensions(); + public JsonSerializerOptions JsonSerializerOptions { get; set; } = GetDefaultJsonSerializerOptions(); /// /// The that is going to be used @@ -32,13 +31,13 @@ public class GraphQLHttpClientOptions { /// /// The that will be send on POST /// - public MediaTypeHeaderValue MediaType { get; set; } = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); // This should be "application/graphql" also "application/x-www-form-urlencoded" is Accepted + public string MediaType { get; set; } = "application/json"; // This should be "application/graphql" also "application/x-www-form-urlencoded" is Accepted /// /// The back-off strategy for automatic websocket/subscription reconnects. Calculates the delay before the next connection attempt is made.
/// default formula: min(n, 5) * 1,5 * random(0.0, 1.0) ///
- public Func BackOffStrategy = n => { + public Func BackOffStrategy { get; set; } = n => { var rnd = new Random(); return TimeSpan.FromSeconds(Math.Min(n, 5) * 1.5 + rnd.NextDouble()); }; @@ -53,5 +52,24 @@ public class GraphQLHttpClientOptions { ///
public Func> PreprocessRequest { get; set; } = (request, client) => Task.FromResult(request); + /// + /// Generates the default + /// + /// + public static JsonSerializerOptions GetDefaultJsonSerializerOptions() { + var options = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + //options.Converters.Add(new JsonSerializerOptionsState(options)); + //options.Converters.Add(new DictionaryConverterFactory()); + //options.Converters.Add(new CollectionConverterFactory()); + //options.Converters.Add(new JsonNodeConverterFactory()); + //options.Converters.Add(new ObjectConverterFactory()); + + options.SetupExtensions(); + + return options; + } } } diff --git a/src/GraphQL.Client.Http/GraphQLSerializationExtensions.cs b/src/GraphQL.Client.Http/GraphQLSerializationExtensions.cs index a0a6d271..758a8245 100644 --- a/src/GraphQL.Client.Http/GraphQLSerializationExtensions.cs +++ b/src/GraphQL.Client.Http/GraphQLSerializationExtensions.cs @@ -2,6 +2,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Dahomey.Json; +using Dahomey.Json.Serialization.Converters.Factories; namespace GraphQL.Client.Http { public static class GraphQLSerializationExtensions { @@ -21,5 +23,14 @@ public static ValueTask DeserializeFromJsonAsync(stream, options.JsonSerializerOptions, cancellationToken); } + public static JsonSerializerOptions SetupDahomeyJson(this JsonSerializerOptions options) { + options.Converters.Add(new JsonSerializerOptionsState(options)); + options.Converters.Add(new DictionaryConverterFactory()); + options.Converters.Add(new CollectionConverterFactory()); + //options.Converters.Add(new JsonNodeConverterFactory()); + options.Converters.Add(new ObjectConverterFactory()); + + return options; + } } } diff --git a/src/GraphQL.Client/GraphQL.Client.csproj b/src/GraphQL.Client/GraphQL.Client.csproj index 78633fd1..1c883480 100644 --- a/src/GraphQL.Client/GraphQL.Client.csproj +++ b/src/GraphQL.Client/GraphQL.Client.csproj @@ -12,6 +12,7 @@ + diff --git a/src/GraphQL.Client/GraphQLResponse.cs b/src/GraphQL.Client/GraphQLResponse.cs index 81c163d5..64c28ac5 100644 --- a/src/GraphQL.Client/GraphQLResponse.cs +++ b/src/GraphQL.Client/GraphQLResponse.cs @@ -1,23 +1,28 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; namespace GraphQL.Client { public class GraphQLResponse : IEquatable?> { + [JsonPropertyName("data")] public T Data { get; set; } + [JsonPropertyName("errors")] public GraphQLError[]? Errors { get; set; } - public IDictionary? Extensions { get; set; } + [JsonPropertyName("extensions")] + public JsonElement? Extensions { get; set; } public override bool Equals(object? obj) => this.Equals(obj as GraphQLResponse); public bool Equals(GraphQLResponse? other) { if (other == null) { return false; } if (ReferenceEquals(this, other)) { return true; } - if (!EqualityComparer.Default.Equals(this.Data, other.Data)) { return false; } + if (!EqualityComparer.Default.Equals(this.Data, other.Data)) { return false; } { if (this.Errors != null && other.Errors != null) { if (!Enumerable.SequenceEqual(this.Errors, other.Errors)) { return false; } @@ -25,13 +30,13 @@ 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; } + if (!EqualityComparer.Default.Equals(this.Extensions, other.Extensions)) { return false; } return true; } public override int GetHashCode() { unchecked { - var hashCode = EqualityComparer.Default.GetHashCode(this.Data); + var hashCode = EqualityComparer.Default.GetHashCode(this.Data); { if (this.Errors != null) { foreach (var element in this.Errors) { @@ -42,7 +47,7 @@ public override int GetHashCode() { hashCode = (hashCode * 397) ^ 0; } } - hashCode = (hashCode * 397) ^ EqualityComparer?>.Default.GetHashCode(this.Extensions); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(this.Extensions); return hashCode; } } diff --git a/src/GraphQL.Primitives/GraphQL.Primitives.csproj b/src/GraphQL.Primitives/GraphQL.Primitives.csproj index ca03991d..aecb7aea 100644 --- a/src/GraphQL.Primitives/GraphQL.Primitives.csproj +++ b/src/GraphQL.Primitives/GraphQL.Primitives.csproj @@ -5,11 +5,11 @@ GraphQL basic types GraphQL - netstandard1.0;netstandard2.0 + netstandard2.0 + - diff --git a/src/GraphQL.Primitives/GraphQLError.cs b/src/GraphQL.Primitives/GraphQLError.cs index 6ccdd39c..82a80ddc 100644 --- a/src/GraphQL.Primitives/GraphQLError.cs +++ b/src/GraphQL.Primitives/GraphQLError.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; namespace GraphQL { @@ -11,23 +13,27 @@ public class GraphQLError : IEquatable { /// /// The extensions of the error - /// - public IDictionary? Extensions { get; set; } + /// + [JsonPropertyName("extensions")] + public JsonElement? Extensions { get; set; } /// /// The locations of the error /// + [JsonPropertyName("locations")] public GraphQLLocation[]? Locations { get; set; } /// /// The message of the error /// + [JsonPropertyName("message")] public string Message { get; set; } /// /// The Path of the error /// - public dynamic[]? Path { get; set; } + [JsonPropertyName("path")] + public object[]? Path { get; set; } /// /// Returns a value that indicates whether this instance is equal to a specified object @@ -45,7 +51,7 @@ 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 (!EqualityComparer.Default.Equals(this.Extensions, other.Extensions)) { return false; } { if (this.Locations != null && other.Locations != null) { if (!this.Locations.SequenceEqual(other.Locations)) { return false; } @@ -70,7 +76,7 @@ public bool Equals(GraphQLError? other) { public override int GetHashCode() { var hashCode = 0; if (this.Extensions != null) { - hashCode = hashCode ^ EqualityComparer>.Default.GetHashCode(this.Extensions); + 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 dd8a180a..95bf8eae 100644 --- a/src/GraphQL.Primitives/GraphQLRequest.cs +++ b/src/GraphQL.Primitives/GraphQLRequest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace GraphQL { @@ -11,16 +12,20 @@ public class GraphQLRequest : IEquatable { /// /// The Query /// + /// + [JsonPropertyName("query")] public string Query { get; set; } /// /// The name of the Operation /// + [JsonPropertyName("operationName")] public string? OperationName { get; set; } /// /// Represents the request variables /// + [JsonPropertyName("variables")] public virtual object? Variables { get; set; } diff --git a/tests/GraphQL.Client.Http.Tests/GraphQL.Client.Http.Tests.csproj b/tests/GraphQL.Client.Http.Tests/GraphQL.Client.Http.Tests.csproj index 64ce8f5e..c0b659da 100644 --- a/tests/GraphQL.Client.Http.Tests/GraphQL.Client.Http.Tests.csproj +++ b/tests/GraphQL.Client.Http.Tests/GraphQL.Client.Http.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/GraphQL.Client.Http.Tests/JsonSerializationTests.cs b/tests/GraphQL.Client.Http.Tests/JsonSerializationTests.cs index 1f12cff5..74191b15 100644 --- a/tests/GraphQL.Client.Http.Tests/JsonSerializationTests.cs +++ b/tests/GraphQL.Client.Http.Tests/JsonSerializationTests.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using Xunit; using System.Text.Json; +using FluentAssertions; namespace GraphQL.Client.Http.Tests { public class JsonSerializationTests { @@ -9,7 +11,9 @@ public void WebSocketResponseDeserialization() { var testObject = new ExtendedTestObject { Id = "test", OtherData = "this is some other stuff" }; var json = JsonSerializer.Serialize(testObject); var deserialized = JsonSerializer.Deserialize(json); - + var dict = JsonSerializer.Deserialize>(json); + var childObject = (JsonElement) dict["ChildObject"]; + childObject.GetProperty("Id").GetString().Should().Be(testObject.ChildObject.Id); } public class TestObject { @@ -19,6 +23,8 @@ public class TestObject { public class ExtendedTestObject : TestObject { public string OtherData { get; set; } + + public TestObject ChildObject{ get; set; } = new TestObject {Id = "1337"}; } } } diff --git a/tests/GraphQL.Integration.Tests/ExtensionsTest.cs b/tests/GraphQL.Integration.Tests/ExtensionsTest.cs new file mode 100644 index 00000000..f3dfa126 --- /dev/null +++ b/tests/GraphQL.Integration.Tests/ExtensionsTest.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using Dahomey.Json; +using FluentAssertions; +using GraphQL.Client; +using GraphQL.Integration.Tests.Extensions; +using GraphQL.Integration.Tests.Helpers; +using IntegrationTestServer; +using IntegrationTestServer.ChatSchema; +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(); + + JsonElement data = new JsonElement(); + response.Errors[0].Extensions.Value.Invoking(element => data = element.GetProperty("data")).Should() + .NotThrow(); + + foreach (var item in ChatQuery.TestExtensions) { + JsonElement value = new JsonElement(); + data.Invoking(element => value = element.GetProperty(item.Key)).Should().NotThrow(); + + switch (item.Value) { + case int i: + value.GetInt32().Should().Be(i); + break; + default: + value.GetString().Should().BeEquivalentTo(item.Value.ToString()); + break; + } + } + } + + [Fact] + public async void DontNeedToUseCamelCaseNamingStrategy() { + + using var setup = SetupTest(); + setup.Client.Options.JsonSerializerOptions = new JsonSerializerOptions().SetupExtensions(); + + 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/GraphQL.Integration.Tests.csproj b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj index de5a92ca..aaf21a67 100644 --- a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj +++ b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs index 64ec475a..ca81340f 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs @@ -1,4 +1,5 @@ using System.Net.Http; +using System.Text.Json; using GraphQL.Client; using GraphQL.Client.Http; using GraphQL.Integration.Tests.Helpers; @@ -25,6 +26,20 @@ public async void QueryTheory(int id, string name) { } } + [Theory] + [ClassData(typeof(StarWarsHumans))] + public async void QueryWithJsonElementAsReturnTypeTheory(int id, string name) { + var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); + + using (var setup = SetupTest()) { + var response = await setup.Client.SendQueryAsync(graphQLRequest) + .ConfigureAwait(false); + + Assert.Null(response.Errors); + Assert.Equal(name, response.Data.GetProperty("human").GetProperty("name").GetString()); + } + } + [Theory] [ClassData(typeof(StarWarsHumans))] public async void QueryWitVarsTheory(int id, string name) { diff --git a/tests/IntegrationTestServer/ChatSchema/CapitalizedFieldsGraphType.cs b/tests/IntegrationTestServer/ChatSchema/CapitalizedFieldsGraphType.cs new file mode 100644 index 00000000..32b235e5 --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/CapitalizedFieldsGraphType.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Threading.Tasks; +using GraphQL.Types; + +namespace IntegrationTestServer.ChatSchema { + public class CapitalizedFieldsGraphType: ObjectGraphType { + public CapitalizedFieldsGraphType() { + Name = "CapitalizedFields"; + + Field() + .Name("StringField") + .Resolve(context => "hello world"); + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/ChatQuery.cs b/tests/IntegrationTestServer/ChatSchema/ChatQuery.cs index 6620c19c..8b423f95 100644 --- a/tests/IntegrationTestServer/ChatSchema/ChatQuery.cs +++ b/tests/IntegrationTestServer/ChatSchema/ChatQuery.cs @@ -1,10 +1,27 @@ +using System.Collections.Generic; using System.Linq; +using GraphQL; using GraphQL.Types; namespace IntegrationTestServer.ChatSchema { public class ChatQuery : ObjectGraphType { + + public static readonly Dictionary TestExtensions = new Dictionary { + {"extension1", "hello world"}, + {"another extension", 4711} + }; + public ChatQuery(IChat chat) { + Name = "ChatQuery"; + Field>("messages", resolve: context => chat.AllMessages.Take(100)); + + Field() + .Name("extensionsTest") + .Resolve(context => { + context.Errors.Add(new ExecutionError("this error contains extension fields", TestExtensions)); + return null; + }); } } }