diff --git a/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs index 0f486482..a44e57d6 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs @@ -8,7 +8,7 @@ namespace GraphQL.Client.Abstractions.Websocket { /// /// A Subscription Request /// - public class GraphQLWebSocketRequest : IEquatable { + public class GraphQLWebSocketRequest : Dictionary, IEquatable { public const string IdKey = "id"; public const string TypeKey = "type"; public const string PayloadKey = "payload"; @@ -16,20 +16,26 @@ public class GraphQLWebSocketRequest : IEquatable { /// /// The Identifier of the Response /// - [DataMember(Name = IdKey)] - public virtual string Id { get; set; } + public string Id { + get => ContainsKey(IdKey) ? (string)this[IdKey] : null; + set => this[IdKey] = value; + } /// /// The Type of the Request /// - [DataMember(Name = TypeKey)] - public virtual string Type { get; set; } + public string Type { + get => ContainsKey(TypeKey) ? (string)this[TypeKey] : null; + set => this[TypeKey] = value; + } /// /// The payload of the websocket request /// - [DataMember(Name = PayloadKey)] - public virtual GraphQLRequest Payload { get; set; } + public GraphQLRequest Payload { + get => ContainsKey(PayloadKey) ? (GraphQLRequest) this[PayloadKey] : null; + set => this[PayloadKey] = value; + } private TaskCompletionSource _tcs = new TaskCompletionSource(); diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs deleted file mode 100644 index cf012c92..00000000 --- a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace GraphQL.Client.Serializer.Newtonsoft { - public class GraphQLRequest: GraphQL.GraphQLRequest { - [JsonProperty(QueryKey)] - public override string Query { get; set; } - [JsonProperty(OperationNameKey)] - public override string? OperationName { get; set; } - [JsonProperty(VariablesKey)] - public override object? Variables { get; set; } - - public GraphQLRequest() { } - - public GraphQLRequest(GraphQL.GraphQLRequest other) { - Query = other.Query; - OperationName = other.OperationName; - Variables = other.Variables; - } - - } -} diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs deleted file mode 100644 index c575b9d3..00000000 --- a/src/GraphQL.Client.Serializer.Newtonsoft/GraphQLWebSocketRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace GraphQL.Client.Serializer.Newtonsoft { - public class GraphQLWebSocketRequest: Abstractions.Websocket.GraphQLWebSocketRequest { - - [JsonProperty(IdKey)] - public override string Id { get; set; } - [JsonProperty(TypeKey)] - public override string Type { get; set; } - [JsonProperty(PayloadKey)] - public override GraphQL.GraphQLRequest Payload { get; set; } - - public GraphQLWebSocketRequest() - { - } - - public GraphQLWebSocketRequest(Abstractions.Websocket.GraphQLWebSocketRequest other) { - Id = other.Id; - Type = other.Type; - Payload = other.Payload != null ? new GraphQLRequest(other.Payload) : null; // create serializer-specific type - } - } -} diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index 7194a547..67679432 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -34,11 +34,11 @@ private void ConfigureMandatorySerializerOptions() { } public string SerializeToString(GraphQL.GraphQLRequest request) { - return JsonConvert.SerializeObject(new GraphQLRequest(request), JsonSerializerSettings); + return JsonConvert.SerializeObject(request, JsonSerializerSettings); } public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest request) { - var json = JsonConvert.SerializeObject(new GraphQLWebSocketRequest(request), JsonSerializerSettings); + var json = JsonConvert.SerializeObject(request, JsonSerializerSettings); return Encoding.UTF8.GetBytes(json); } diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj index 024b0681..8f8bc66a 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj +++ b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj @@ -10,10 +10,6 @@ - - - - diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs deleted file mode 100644 index 1f3b7908..00000000 --- a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace GraphQL.Client.Serializer.SystemTextJson { - public class GraphQLRequest: GraphQL.GraphQLRequest { - [JsonPropertyName(QueryKey)] - public override string Query { get; set; } - [JsonPropertyName(OperationNameKey)] - public override string? OperationName { get; set; } - [JsonPropertyName(VariablesKey)] - public override object? Variables { get; set; } - - public GraphQLRequest() { } - - public GraphQLRequest(GraphQL.GraphQLRequest other) { - Query = other.Query; - OperationName = other.OperationName; - Variables = other.Variables; - } - - } -} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs b/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs deleted file mode 100644 index 4961e00d..00000000 --- a/src/GraphQL.Client.Serializer.SystemTextJson/GraphQLWebSocketRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Text.Json.Serialization; - -namespace GraphQL.Client.Serializer.SystemTextJson { - public class GraphQLWebSocketRequest: Abstractions.Websocket.GraphQLWebSocketRequest { - - [JsonPropertyName(IdKey)] - public override string Id { get; set; } - [JsonPropertyName(TypeKey)] - public override string Type { get; set; } - [JsonPropertyName(PayloadKey)] - public override GraphQL.GraphQLRequest Payload { get; set; } - - public GraphQLWebSocketRequest() - { - } - - public GraphQLWebSocketRequest(Abstractions.Websocket.GraphQLWebSocketRequest other) { - Id = other.Id; - Type = other.Type; - Payload = other.Payload != null ? new GraphQLRequest(other.Payload) : null; // create serializer-specific type; - } - } -} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/ImmutableConverter.cs b/src/GraphQL.Client.Serializer.SystemTextJson/ImmutableConverter.cs new file mode 100644 index 00000000..a4f49f49 --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/ImmutableConverter.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GraphQL.Client.Serializer.SystemTextJson { + + /// + /// class for converting immutable objects, derived from https://github.com/manne/obviously/blob/master/src/system.text.json/Core/ImmutableConverter.cs + /// + public class ImmutableConverter : JsonConverter { + public override bool CanConvert(Type typeToConvert) { + bool result; + var constructors = typeToConvert.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + if (constructors.Length != 1) { + result = false; + } + else { + var constructor = constructors[0]; + var parameters = constructor.GetParameters(); + var hasParameters = parameters.Length > 0; + if (hasParameters) { + var properties = typeToConvert.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + result = true; + foreach (var parameter in parameters) { + var hasMatchingProperty = properties.Any(p => + NameOfPropertyAndParameter.Matches(p.Name, parameter.Name, typeToConvert.IsAnonymous())); + if (!hasMatchingProperty) { + result = false; + break; + } + } + } + else { + result = false; + } + } + + return result; + } + + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + var valueOfProperty = new Dictionary(); + var namedPropertiesMapping = GetNamedProperties(options, GetProperties(typeToConvert)); + reader.Read(); + while (true) { + if (reader.TokenType != JsonTokenType.PropertyName && reader.TokenType != JsonTokenType.String) { + break; + } + + var jsonPropName = reader.GetString(); + var normalizedPropName = ConvertAndNormalizeName(jsonPropName, options); + if (!namedPropertiesMapping.TryGetValue(normalizedPropName, out var obProp)) { + reader.Read(); + } + else { + var value = JsonSerializer.Deserialize(ref reader, obProp.PropertyType, options); + reader.Read(); + valueOfProperty[obProp] = value; + } + } + + var ctor = typeToConvert.GetConstructors(BindingFlags.Public | BindingFlags.Instance).First(); + var parameters = ctor.GetParameters(); + var parameterValues = new object[parameters.Length]; + for (var index = 0; index < parameters.Length; index++) { + var parameterInfo = parameters[index]; + var value = valueOfProperty.First(prop => + NameOfPropertyAndParameter.Matches(prop.Key.Name, parameterInfo.Name, typeToConvert.IsAnonymous())).Value; + + parameterValues[index] = value; + } + + var instance = ctor.Invoke(parameterValues); + return instance; + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { + var strippedOptions = new JsonSerializerOptions { + AllowTrailingCommas = options.AllowTrailingCommas, + DefaultBufferSize = options.DefaultBufferSize, + DictionaryKeyPolicy = options.DictionaryKeyPolicy, + Encoder = options.Encoder, + IgnoreNullValues = options.IgnoreNullValues, + IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties, + MaxDepth = options.MaxDepth, + PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive, + PropertyNamingPolicy = options.PropertyNamingPolicy, + ReadCommentHandling = options.ReadCommentHandling, + WriteIndented = options.WriteIndented + }; + foreach (var converter in options.Converters) { + if (!(converter is ImmutableConverter)) + strippedOptions.Converters.Add(converter); + } + JsonSerializer.Serialize(writer, value, strippedOptions); + } + + private static PropertyInfo[] GetProperties(IReflect typeToConvert) { + return typeToConvert.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + } + + private static IReadOnlyDictionary GetNamedProperties(JsonSerializerOptions options, IEnumerable properties) { + var result = new Dictionary(); + foreach (var property in properties) { + string name; + var nameAttribute = property.GetCustomAttribute(); + if (nameAttribute != null) { + name = nameAttribute.Name; + } + else { + name = options.PropertyNamingPolicy?.ConvertName(property.Name) ?? property.Name; + } + + var normalizedName = NormalizeName(name, options); + result.Add(normalizedName, property); + } + + return result; + } + + private static string ConvertAndNormalizeName(string name, JsonSerializerOptions options) { + var convertedName = options.PropertyNamingPolicy?.ConvertName(name) ?? name; + return options.PropertyNameCaseInsensitive ? convertedName.ToLowerInvariant() : convertedName; + } + + private static string NormalizeName(string name, JsonSerializerOptions options) { + return options.PropertyNameCaseInsensitive ? name.ToLowerInvariant() : name; + } + } + + internal static class NameOfPropertyAndParameter { + public static bool Matches(string propertyName, string parameterName, bool anonymousType) { + if (propertyName is null && parameterName is null) { + return true; + } + + if (propertyName is null || parameterName is null) { + return false; + } + + if (anonymousType) { + return propertyName.Equals(parameterName, StringComparison.Ordinal); + } + else { + var xRight = propertyName.AsSpan(1); + var yRight = parameterName.AsSpan(1); + return char.ToLowerInvariant(propertyName[0]).CompareTo(parameterName[0]) == 0 && xRight.Equals(yRight, StringComparison.Ordinal); + } + } + } + + internal static class TypeExtensions { + // copied from https://github.com/dahomey-technologies/Dahomey.Json/blob/master/src/Dahomey.Json/Util/TypeExtensions.cs + public static bool IsAnonymous(this Type type) { + return type.Namespace == null + && type.IsSealed + && type.BaseType == typeof(object) + && !type.IsPublic + && type.IsDefined(typeof(CompilerGeneratedAttribute), false); + } + } +} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs b/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs new file mode 100644 index 00000000..b4cc2614 --- /dev/null +++ b/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,11 @@ +using System.Text.Json; + +namespace GraphQL.Client.Serializer.SystemTextJson { + public static class JsonSerializerOptionsExtensions { + public static JsonSerializerOptions SetupImmutableConverter( + this JsonSerializerOptions options) { + options.Converters.Add(new ImmutableConverter()); + return options; + } + } +} diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index 62be2e37..0f3b1e2c 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -3,7 +3,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Dahomey.Json; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; @@ -13,8 +12,8 @@ public class SystemTextJsonSerializer: IGraphQLWebsocketJsonSerializer { public static JsonSerializerOptions DefaultJsonSerializerOptions => new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }.SetupExtensions(); - + }.SetupImmutableConverter(); + public JsonSerializerOptions Options { get; } public SystemTextJsonSerializer() : this(DefaultJsonSerializerOptions) { } @@ -34,7 +33,7 @@ private void ConfigureMandatorySerializerOptions() { } public string SerializeToString(GraphQL.GraphQLRequest request) { - return JsonSerializer.Serialize(new GraphQLRequest(request), Options); + return JsonSerializer.Serialize(request, Options); } public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) { @@ -42,7 +41,7 @@ public Task> DeserializeFromUtf8StreamAsync DeserializeToWebsocketResponseWrapperAsync(Stream stream) { diff --git a/src/GraphQL.Primitives/GraphQLRequest.cs b/src/GraphQL.Primitives/GraphQLRequest.cs index f7e48e24..ed351a10 100644 --- a/src/GraphQL.Primitives/GraphQLRequest.cs +++ b/src/GraphQL.Primitives/GraphQLRequest.cs @@ -7,7 +7,7 @@ namespace GraphQL { /// /// A GraphQL request /// - public class GraphQLRequest : IEquatable { + public class GraphQLRequest : Dictionary, IEquatable { public const string OperationNameKey = "operationName"; public const string QueryKey = "query"; public const string VariablesKey = "variables"; @@ -15,20 +15,26 @@ public class GraphQLRequest : IEquatable { /// /// The Query /// - [DataMember(Name = QueryKey)] - public virtual string Query { get; set; } + public string Query { + get => ContainsKey(QueryKey) ? (string) this[QueryKey] : null; + set => this[QueryKey] = value; + } /// /// The name of the Operation /// - [DataMember(Name = OperationNameKey)] - public virtual string? OperationName { get; set; } + public string? OperationName { + get => ContainsKey(OperationNameKey) ? (string)this[OperationNameKey] : null; + set => this[OperationNameKey] = value; + } /// /// Represents the request variables /// - [DataMember(Name = VariablesKey)] - public virtual object? Variables { get; set; } + public object? Variables { + get => ContainsKey(VariablesKey) ? this[VariablesKey] : null; + set => this[VariablesKey] = value; + } public GraphQLRequest() { } @@ -58,9 +64,7 @@ public override bool Equals(object? obj) { public virtual bool Equals(GraphQLRequest? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Query == other.Query - && OperationName == other.OperationName - && EqualityComparer.Default.Equals(Variables, other.Variables); + return EqualityComparer>.Default.Equals(this, other); } /// diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs index 579e756e..26d44472 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs @@ -75,8 +75,7 @@ query Droid($id: String!) { new { id = id.ToString() }, "Human"); - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - ; + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); diff --git a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs index 84bb5e84..95c18908 100644 --- a/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using GraphQL.Client.Serializer.Newtonsoft; using Newtonsoft.Json; diff --git a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs index a3d393dc..76f9ff78 100644 --- a/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs +++ b/tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Dahomey.Json; using GraphQL.Client.Serializer.SystemTextJson; namespace GraphQL.Client.Serializer.Tests { @@ -9,6 +8,6 @@ public SystemTextJsonSerializerTests() : base(new SystemTextJsonSerializer()) { public class SystemTextJsonSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest { public SystemTextJsonSerializeNoCamelCaseTest() - : base(new SystemTextJsonSerializer(new JsonSerializerOptions().SetupExtensions())) { } + : base(new SystemTextJsonSerializer(new JsonSerializerOptions().SetupImmutableConverter())) { } } } diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs index 41d15ce0..a506cb6d 100644 --- a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs @@ -7,7 +7,7 @@ namespace GraphQL.Client.Serializer.Tests.TestData { public class SerializeToBytesTestData : IEnumerable { public IEnumerator GetEnumerator() { yield return new object[] { - "{\"id\":\"1234567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":null}}", + "{\"id\":\"1234567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null}}", new GraphQLWebSocketRequest { Id = "1234567", Type = GraphQLWebSocketMessageType.GQL_START, @@ -15,7 +15,7 @@ public IEnumerator GetEnumerator() { } }; yield return new object[] { - "{\"id\":\"34476567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"}}}", + "{\"id\":\"34476567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"},\"operationName\":null}}", new GraphQLWebSocketRequest { Id = "34476567", Type = GraphQLWebSocketMessageType.GQL_START, diff --git a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs index 86d1ae9f..404fda64 100644 --- a/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs +++ b/tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs @@ -6,13 +6,17 @@ namespace GraphQL.Client.Serializer.Tests.TestData { public class SerializeToStringTestData : IEnumerable { public IEnumerator GetEnumerator() { yield return new object[] { - "{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":null}", + "{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null}", new GraphQLRequest("simple query string") }; yield return new object[] { - "{\"query\":\"simplequerystring\",\"operationName\":null,\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"}}", + "{\"query\":\"simplequerystring\",\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"},\"operationName\":null}", new GraphQLRequest("simple query string", new { camelCaseProperty = "camelCase", PascalCaseProperty = "PascalCase"}) }; + yield return new object[] { + "{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null,\"authentication\":\"an-authentication-token\"}", + new GraphQLRequest("simple query string"){{"authentication", "an-authentication-token"}} + }; } IEnumerator IEnumerable.GetEnumerator() {