Skip to content
Merged
4 changes: 2 additions & 2 deletions GraphQLSharp.Tests/GraphQLClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,9 @@ public async Task QueryWithAliases()
var response = await _client.ExecuteAsync(request);
//response.data is JsonElement
var myProducts = response.data.Value.GetProperty("myProducts")
.Deserialize<ProductConnection>(Serializer.Options);
.Deserialize<ProductConnection>(Serializer.GetOptions());
var myOrders = response.data.Value.GetProperty("myOrders")
.Deserialize<OrderConnection>(Serializer.Options);
.Deserialize<OrderConnection>(Serializer.GetOptions());
Assert.IsNotNull(myProducts.nodes.FirstOrDefault()?.title);
Assert.IsNotNull(myOrders.nodes.FirstOrDefault()?.name);
}
Expand Down
54 changes: 49 additions & 5 deletions GraphQLSharp.Tests/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace GraphQLSharp.Tests;
[TestClass]
public class SerializationTests
{
private class MyObject
private class MyDateTimeObject
{
public DateTime at { get; set; }
public DateTime? atNullable { get; set; }
Expand All @@ -16,6 +16,13 @@ private class MyObject
public DateTimeOffset? atOffsetNullable2 { get; set; }
}

private class MyBigIntObject
{
public long longValue { get; set; }
public ulong ulongValue { get; set; }
public int intValue { get; set; }
public uint uintValue { get; set; }
}

[TestMethod]
public void DeserializeDateTimePropertyValid()
Expand All @@ -32,7 +39,7 @@ public void DeserializeDateTimePropertyValid()
}
""";

MyObject result = JsonSerializer.Deserialize<MyObject>(json, Serializer.Options);
MyDateTimeObject result = JsonSerializer.Deserialize<MyDateTimeObject>(json, Serializer.GetOptions());
Assert.AreEqual(now, result.at);
Assert.AreEqual(now, result.atNullable);
Assert.AreEqual(nowOffset, result.atOffset);
Expand All @@ -52,7 +59,7 @@ public void DeserializeDateTimePropertyMinValue()
"atOffsetNullable2": null
}
""";
MyObject result = JsonSerializer.Deserialize<MyObject>(json, Serializer.Options);
MyDateTimeObject result = JsonSerializer.Deserialize<MyDateTimeObject>(json, Serializer.GetOptions());
Assert.AreEqual(DateTime.MinValue, result.at);
Assert.AreEqual(DateTime.MinValue, result.atNullable);
Assert.IsNull(result.atNullable2);
Expand All @@ -68,9 +75,46 @@ public void DeserializeDateTimePropertyInvalid()
string json = """
{
"at": "invalid-date",
"atNullable": "invalid-date",
"atNullable": "invalid-date"
}
""";
JsonSerializer.Deserialize<MyDateTimeObject>(json, Serializer.GetOptions());
}

[TestMethod]
public void DeserializeBigInt()
{
string json = $$"""
{
"longValue": "9223372036854775807",
"ulongValue": "18446744073709551615",
"intValue": 2147483647,
"uintValue": 4294967295
}
""";
JsonSerializer.Deserialize<MyObject>(json, Serializer.Options);

MyBigIntObject result = JsonSerializer.Deserialize<MyBigIntObject>(json, Serializer.GetOptions());
Assert.AreEqual(9223372036854775807, result.longValue);
Assert.AreEqual(18446744073709551615, result.ulongValue);
Assert.AreEqual(2147483647, result.intValue);
Assert.AreEqual(4294967295, result.uintValue);
}

[TestMethod]
public void SerializeBigInt()
{
var obj = new MyBigIntObject
{
longValue = 9223372036854775807,
ulongValue = 18446744073709551615,
intValue = 2147483647,
uintValue = 4294967295
};

string json = JsonSerializer.Serialize(obj, Serializer.GetOptions());
Assert.IsTrue(json.Contains(@"""longValue"":""9223372036854775807"""));
Assert.IsTrue(json.Contains(@"""ulongValue"":""18446744073709551615"""));
Assert.IsTrue(json.Contains(@"""intValue"":2147483647"));
Assert.IsTrue(json.Contains(@"""uintValue"":4294967295"));
}
}
4 changes: 2 additions & 2 deletions GraphQLSharp/Client/GraphQLClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private async Task<GraphQLResponse<T>> ExecuteCoreAsync<T>(TRequest request, Can
GraphQLResponse<T> res;
try
{
res = await httpResponseMsg.Content.ReadFromJsonAsync<GraphQLResponse<T>>(_options.JsonSerializerOptions ?? Serializer.Options, cancellationToken);
res = await httpResponseMsg.Content.ReadFromJsonAsync<GraphQLResponse<T>>(_options.JsonSerializerOptions ?? Serializer.GetOptions(), cancellationToken);
if (res == null)
throw new GraphQLException(request, httpResponse, $"Failed to deserialize null GraphQL response. Request: {request}");
}
Expand Down Expand Up @@ -139,7 +139,7 @@ private HttpRequestMessage CreateHttpRequest(TRequest request)
{
Method = HttpMethod.Post,
RequestUri = uri,
Content = JsonContent.Create(request, options: _options.JsonSerializerOptions ?? Serializer.Options),
Content = JsonContent.Create(request, options: _options.JsonSerializerOptions ?? Serializer.GetOptions()),
};

requestMessage.Headers.UserAgent.Add(_defaultUserAgent);
Expand Down
1 change: 1 addition & 0 deletions GraphQLSharp/Client/Options/GraphQLClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public GraphQLClientOptions(Uri uri)

/// <summary>
/// An (optional) JSON serializer options to be used for serializing and deserializing GraphQL requests and responses.
/// If null, default options are used which include converters for DateTime, DateTimeOffset, enums and (u)int64 values.
/// </summary>
public JsonSerializerOptions JsonSerializerOptions { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion GraphQLSharp/GraphQLSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<PackageProjectUrl>https://github.com/Wish-Org/GraphQLSharp</PackageProjectUrl>
<RepositoryUrl>https://github.com/Wish-Org/GraphQLSharp</RepositoryUrl>
<Description>.NET Client for GraphQL - Modern and fast</Description>
<Version>2.20.0</Version>
<Version>2.21.0</Version>
<PackageTags>graphql;client;graphql-client;graphql-generator</PackageTags>
<Authors>Wish-Org</Authors>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand Down
28 changes: 28 additions & 0 deletions GraphQLSharp/Serialization/LongToStringConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

public class Int64ToStringConverter : LongToStringConverter<long>
{
}

public class UInt64ToStringConverter : LongToStringConverter<ulong>
{
}

public abstract class LongToStringConverter<T> : JsonConverter<T>
{
private readonly static JsonConverter<T> _defaultConverter = (JsonConverter<T>)JsonSerializerOptions.Default.GetConverter(typeof(T));
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
return (T)Convert.ChangeType(reader.GetString(), typeof(T), CultureInfo.InvariantCulture);

return _defaultConverter.Read(ref reader, typeToConvert, options);
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStringValue(((IFormattable)value).ToString(null, CultureInfo.InvariantCulture));
}
}
47 changes: 29 additions & 18 deletions GraphQLSharp/Serialization/Serializer.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,61 @@
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Concurrent;

namespace GraphQLSharp;

#nullable enable

public static class Serializer
{
public static readonly JsonSerializerOptions Options;

public static readonly JsonSerializerOptions OptionsIndented;
private static readonly ConcurrentDictionary<(bool indent, bool serializeInt64ToString), JsonSerializerOptions> _optionsToJsonOptions = new();

static Serializer()
{
Options = new JsonSerializerOptions
}

public static JsonSerializerOptions CreateOptions(bool indent, bool serializeInt64ToString)
{
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString,
Converters = { new JsonStringEnumConverter() },
Converters = {
new JsonStringEnumConverter(),
new SafeDateTimeConverter(),
new SafeDateTimeOffsetConverter()
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
Options.Converters.Add(new SafeDateTimeConverter());
Options.Converters.Add(new SafeDateTimeOffsetConverter());

OptionsIndented = new JsonSerializerOptions(Options)
if (serializeInt64ToString)
{
WriteIndented = true
};
options.Converters.Add(new Int64ToStringConverter());
options.Converters.Add(new UInt64ToStringConverter());
}

if (indent)
options.WriteIndented = true;

return options;
}

public static JsonSerializerOptions GetOptions(bool indent)
public static JsonSerializerOptions GetOptions(bool indent = false, bool serializeInt64ToString = true)
{
return indent ? OptionsIndented : Options;
return _optionsToJsonOptions.GetOrAdd((indent, serializeInt64ToString), _ => CreateOptions(indent, serializeInt64ToString));
}

public static string Serialize(object obj, bool indent = false)
public static string Serialize(object obj, bool indent = false, bool serializeInt64ToString = true)
{
return JsonSerializer.Serialize(obj, obj.GetType(), GetOptions(indent));
return JsonSerializer.Serialize(obj, obj.GetType(), GetOptions(indent, serializeInt64ToString));
}

public static object? Deserialize(string json, Type type, bool indent = false)
public static object? Deserialize(string json, Type type, bool indent = false, bool serializeInt64ToString = true)
{
return JsonSerializer.Deserialize(json, type, GetOptions(indent));
return JsonSerializer.Deserialize(json, type, GetOptions(indent, serializeInt64ToString));
}

public static T? Deserialize<T>(string json, bool indent = false)
public static T? Deserialize<T>(string json, bool indent = false, bool serializeInt64ToString = true)
{
return JsonSerializer.Deserialize<T>(json, GetOptions(indent));
return JsonSerializer.Deserialize<T>(json, GetOptions(indent, serializeInt64ToString));
}
}
Loading