From 94cb2caf0bfd20edf64f8b3f3162bb065fefbf8a Mon Sep 17 00:00:00 2001 From: Jkorf Date: Tue, 15 Oct 2024 10:51:19 +0200 Subject: [PATCH] Added System.Text.Json ArrayConverter Write implementation --- .../SystemTextJsonConverterTests.cs | 78 +++++++++++++++++++ .../SystemTextJson/ArrayConverter.cs | 75 +++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs index d024b2d..ac390a5 100644 --- a/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs +++ b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs @@ -5,6 +5,8 @@ using NUnit.Framework; using System; using System.Text.Json.Serialization; using NUnit.Framework.Legacy; +using CryptoExchange.Net.Converters; +using CryptoExchange.Net.Testing.Comparers; namespace CryptoExchange.Net.UnitTests { @@ -242,6 +244,44 @@ namespace CryptoExchange.Net.UnitTests var result = JsonSerializer.Deserialize("{ \"test\": " + value + "}"); Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected)); } + + [Test()] + public void TestArrayConverter() + { + var data = new Test() + { + Prop1 = 2, + Prop2 = null, + Prop3 = "123", + Prop3Again = "123", + Prop4 = null, + Prop5 = new Test2 + { + Prop21 = 3, + Prop22 = "456" + }, + Prop6 = new Test3 + { + Prop31 = 4, + Prop32 = "789" + }, + Prop7 = TestEnum.Two + }; + + var serialized = JsonSerializer.Serialize(data); + var deserialized = JsonSerializer.Deserialize(serialized); + + Assert.That(deserialized.Prop1, Is.EqualTo(2)); + Assert.That(deserialized.Prop2, Is.Null); + Assert.That(deserialized.Prop3, Is.EqualTo("123")); + Assert.That(deserialized.Prop3Again, Is.EqualTo("123")); + Assert.That(deserialized.Prop4, Is.Null); + Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3)); + Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456")); + Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4)); + Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789")); + Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two)); + } } public class STJDecimalObject @@ -281,4 +321,42 @@ namespace CryptoExchange.Net.UnitTests [JsonConverter(typeof(BoolConverter))] public bool Value { get; set; } } + + [JsonConverter(typeof(ArrayConverter))] + record Test + { + [ArrayProperty(0)] + public int Prop1 { get; set; } + [ArrayProperty(1)] + public int? Prop2 { get; set; } + [ArrayProperty(2)] + public string Prop3 { get; set; } + [ArrayProperty(2)] + public string Prop3Again { get; set; } + [ArrayProperty(3)] + public string Prop4 { get; set; } + [ArrayProperty(4)] + public Test2 Prop5 { get; set; } + [ArrayProperty(5)] + public Test3 Prop6 { get; set; } + [ArrayProperty(6), JsonConverter(typeof(EnumConverter))] + public TestEnum? Prop7 { get; set; } + } + + [JsonConverter(typeof(ArrayConverter))] + record Test2 + { + [ArrayProperty(0)] + public int Prop21 { get; set; } + [ArrayProperty(1)] + public string Prop22 { get; set; } + } + + record Test3 + { + [JsonPropertyName("prop31")] + public int Prop31 { get; set; } + [JsonPropertyName("prop32")] + public string Prop32 { get; set; } + } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs index bdb4448..e5567a2 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs @@ -42,8 +42,63 @@ namespace CryptoExchange.Net.Converters.SystemTextJson public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - // TODO - throw new NotImplementedException(); + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartArray(); + + var valueType = value.GetType(); + if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes)) + typeAttributes = CacheTypeAttributes(valueType); + + var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index); + var last = -1; + foreach (var prop in ordered) + { + if (prop.ArrayProperty.Index == last) + continue; + + while (prop.ArrayProperty.Index != last + 1) + { + writer.WriteNullValue(); + last += 1; + } + + last = prop.ArrayProperty.Index; + + var objValue = prop.PropertyInfo.GetValue(value); + if (objValue == null) + { + writer.WriteNullValue(); + continue; + } + + JsonSerializerOptions? typeOptions = null; + if (prop.JsonConverterType != null) + { + var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType); + typeOptions = new JsonSerializerOptions(); + typeOptions.Converters.Clear(); + typeOptions.Converters.Add(converter); + } + + if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType)) + { + if (prop.PropertyInfo.PropertyType == typeof(string)) + writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); + else + writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); + } + else + { + JsonSerializer.Serialize(writer, objValue, typeOptions ?? options); + } + } + + writer.WriteEndArray(); } /// @@ -56,6 +111,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson return (T)ParseObject(ref reader, result, typeToConvert); } + private static bool IsSimple(Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + // nullable type, check if the nested type is simple. + return IsSimple(type.GetGenericArguments()[0]); + } + return type.IsPrimitive + || type.IsEnum + || type == typeof(string) + || type == typeof(decimal); + } + private static List CacheTypeAttributes(Type type) { var attributes = new List(); @@ -71,7 +139,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson ArrayProperty = att, PropertyInfo = property, DefaultDeserialization = property.GetCustomAttribute() != null, - JsonConverterType = property.GetCustomAttribute()?.ConverterType, + JsonConverterType = property.GetCustomAttribute()?.ConverterType ?? property.PropertyType.GetCustomAttribute()?.ConverterType, TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType }); } @@ -126,6 +194,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson JsonTokenType.True => true, JsonTokenType.String => reader.GetString(), JsonTokenType.Number => reader.GetDecimal(), + JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType), _ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"), }; }