diff --git a/CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs b/CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs index d6d56e0..c23651b 100644 --- a/CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs +++ b/CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs @@ -235,6 +235,7 @@ namespace CryptoExchange.Net.UnitTests } [JsonConverter(typeof(EnumConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(CryptoExchange.Net.Converters.SystemTextJson.EnumConverter))] public enum TestEnum { [Map("1")] diff --git a/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs index ac390a5..d3db64a 100644 --- a/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs +++ b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs @@ -146,7 +146,7 @@ namespace CryptoExchange.Net.UnitTests public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected) { var val = value == null ? "null" : $"\"{value}\""; - var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext())); Assert.That(output.Value == expected); } @@ -171,8 +171,8 @@ namespace CryptoExchange.Net.UnitTests [TestCase("three", TestEnum.Three)] [TestCase("Four", TestEnum.Four)] [TestCase("four", TestEnum.Four)] - [TestCase("Four1", TestEnum.One)] - [TestCase(null, TestEnum.One)] + [TestCase("Four1", null)] + [TestCase(null, null)] public void TestEnumConverterParseStringTests(string value, TestEnum? expected) { var result = EnumConverter.ParseString(value); @@ -194,7 +194,7 @@ namespace CryptoExchange.Net.UnitTests public void TestBoolConverter(string value, bool? expected) { var val = value == null ? "null" : $"\"{value}\""; - var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext())); Assert.That(output.Value == expected); } @@ -213,7 +213,7 @@ namespace CryptoExchange.Net.UnitTests public void TestBoolConverterNotNullable(string value, bool expected) { var val = value == null ? "null" : $"\"{value}\""; - var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext())); Assert.That(output.Value == expected); } @@ -265,7 +265,11 @@ namespace CryptoExchange.Net.UnitTests Prop31 = 4, Prop32 = "789" }, - Prop7 = TestEnum.Two + Prop7 = TestEnum.Two, + TestInternal = new Test + { + Prop1 = 10 + } }; var serialized = JsonSerializer.Serialize(data); @@ -281,6 +285,7 @@ namespace CryptoExchange.Net.UnitTests Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4)); Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789")); Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two)); + Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10)); } } @@ -300,29 +305,25 @@ namespace CryptoExchange.Net.UnitTests public class STJEnumObject { - [JsonConverter(typeof(EnumConverter))] public TestEnum? Value { get; set; } } public class NotNullableSTJEnumObject { - [JsonConverter(typeof(EnumConverter))] public TestEnum Value { get; set; } } public class STJBoolObject { - [JsonConverter(typeof(BoolConverter))] public bool? Value { get; set; } } public class NotNullableSTJBoolObject { - [JsonConverter(typeof(BoolConverter))] public bool Value { get; set; } } - [JsonConverter(typeof(ArrayConverter))] + [JsonConverter(typeof(ArrayConverter))] record Test { [ArrayProperty(0)] @@ -339,11 +340,13 @@ namespace CryptoExchange.Net.UnitTests public Test2 Prop5 { get; set; } [ArrayProperty(5)] public Test3 Prop6 { get; set; } - [ArrayProperty(6), JsonConverter(typeof(EnumConverter))] + [ArrayProperty(6), JsonConverter(typeof(EnumConverter))] public TestEnum? Prop7 { get; set; } + [ArrayProperty(7)] + public Test TestInternal { get; set; } } - [JsonConverter(typeof(ArrayConverter))] + [JsonConverter(typeof(ArrayConverter))] record Test2 { [ArrayProperty(0)] @@ -359,4 +362,17 @@ namespace CryptoExchange.Net.UnitTests [JsonPropertyName("prop32")] public string Prop32 { get; set; } } + + [JsonSerializable(typeof(Test))] + [JsonSerializable(typeof(Test2))] + [JsonSerializable(typeof(Test3))] + [JsonSerializable(typeof(NotNullableSTJBoolObject))] + [JsonSerializable(typeof(STJBoolObject))] + [JsonSerializable(typeof(NotNullableSTJEnumObject))] + [JsonSerializable(typeof(STJEnumObject))] + [JsonSerializable(typeof(STJDecimalObject))] + [JsonSerializable(typeof(STJTimeObject))] + internal partial class SerializationContext : JsonSerializerContext + { + } } diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index 8f16d26..c248ba4 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -49,26 +49,5 @@ namespace CryptoExchange.Net.Authentication { return new ApiCredentials(Key, Secret, CredentialType); } - - /// - /// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret - /// - /// The stream containing the json data - /// A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'. - /// A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'. - public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null) - { - var accessor = new SystemTextJsonStreamMessageAccessor(); - if (!accessor.Read(inputStream, false).Result) - throw new ArgumentException("Input stream not valid json data"); - - var key = accessor.GetValue(MessagePath.Get().Property(identifierKey ?? "apiKey")); - var secret = accessor.GetValue(MessagePath.Get().Property(identifierSecret ?? "apiSecret")); - if (key == null || secret == null) - throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); - - inputStream.Seek(0, SeekOrigin.Begin); - return new ApiCredentials(key, secret); - } } } diff --git a/CryptoExchange.Net/CommonObjects/Balance.cs b/CryptoExchange.Net/CommonObjects/Balance.cs index 696a35c..8d279bc 100644 --- a/CryptoExchange.Net/CommonObjects/Balance.cs +++ b/CryptoExchange.Net/CommonObjects/Balance.cs @@ -1,4 +1,6 @@ -namespace CryptoExchange.Net.CommonObjects +using CryptoExchange.Net.Converters.SystemTextJson; + +namespace CryptoExchange.Net.CommonObjects { /// /// Balance data diff --git a/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs index 79ec7f5..2388a8d 100644 --- a/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs +++ b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs @@ -7,6 +7,6 @@ namespace CryptoExchange.Net.Converters.JsonNet public class JsonNetMessageSerializer : IMessageSerializer { /// - public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None); + public string Serialize(T message) => JsonConvert.SerializeObject(message, Formatting.None); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs index 5fe381b..a3b9fff 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs @@ -7,6 +7,7 @@ using System.Text.Json.Serialization; using System.Text.Json; using CryptoExchange.Net.Attributes; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace CryptoExchange.Net.Converters.SystemTextJson { @@ -14,215 +15,226 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties /// with [ArrayProperty(x)] where x is the index of the property in the array /// - public class ArrayConverter : JsonConverterFactory +#if NET5_0_OR_GREATER + public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TContext> : JsonConverter where T : new() where TContext: JsonSerializerContext +# else + public class ArrayConverter : JsonConverter where T : new() where TContext: JsonSerializerContext +#endif { - /// - public override bool CanConvert(Type typeToConvert) => true; + private static readonly ConcurrentDictionary> _typeAttributesCache = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary _converterOptionsCache = new ConcurrentDictionary(); /// - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert); - return (JsonConverter)Activator.CreateInstance(converterType)!; + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartArray(); + + var valueType = typeof(T); + 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.JsonConverter != null) + { + typeOptions = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + TypeInfoResolver = (TContext)Activator.CreateInstance(typeof(TContext))!, + }; + typeOptions.Converters.Add(prop.JsonConverter); + } + + if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType)) + { + if (prop.TargetType == typeof(string)) + writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); + else if (prop.TargetType == typeof(bool)) + writer.WriteBooleanValue((bool)objValue); + else + writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!); + } + else + { + JsonSerializer.Serialize(writer, objValue, typeOptions ?? options); + } + } + + writer.WriteEndArray(); + } + + /// + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return default; + + var result = Activator.CreateInstance(typeof(T))!; + return (T)ParseObject(ref reader, result, typeof(T), options); + } + + 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); + } + +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + private static List CacheTypeAttributes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type) +#else + private static List CacheTypeAttributes(Type type) +#endif + { + var attributes = new List(); + var properties = type.GetProperties(); + foreach (var property in properties) + { + var att = property.GetCustomAttribute(); + if (att == null) + continue; + + var converterType = property.GetCustomAttribute()?.ConverterType ?? property.PropertyType.GetCustomAttribute()?.ConverterType; + attributes.Add(new ArrayPropertyInfo + { + ArrayProperty = att, + PropertyInfo = property, + DefaultDeserialization = property.GetCustomAttribute() != null, + JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!, + TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType + }); + } + + _typeAttributesCache.TryAdd(type, attributes); + return attributes; + } + + +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + private static object ParseObject(ref Utf8JsonReader reader, object result, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type objectType, JsonSerializerOptions options) +#else + private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options) +#endif + { + if (reader.TokenType != JsonTokenType.StartArray) + throw new Exception("Not an array"); + + if (!_typeAttributesCache.TryGetValue(objectType, out var attributes)) + attributes = CacheTypeAttributes(objectType); + + int index = 0; + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + break; + + var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index); + if (!indexAttributes.Any()) + { + index++; + continue; + } + + foreach (var attribute in indexAttributes) + { + var targetType = attribute.TargetType; + object? value = null; + if (attribute.JsonConverter != null) + { + if (!_converterOptionsCache.TryGetValue(attribute.JsonConverter, out var newOptions)) + { + newOptions = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + Converters = { attribute.JsonConverter }, + TypeInfoResolver = options.TypeInfoResolver, + }; + _converterOptionsCache.TryAdd(attribute.JsonConverter, newOptions); + } + + value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions); + } + else if (attribute.DefaultDeserialization) + { + // Use default deserialization + value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters((TContext)Activator.CreateInstance(typeof(TContext))!)); + } + else + { + value = reader.TokenType switch + { + JsonTokenType.Null => null, + JsonTokenType.False => false, + JsonTokenType.True => true, + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetDecimal(), + JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options), + _ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"), + }; + } + + if (targetType.IsAssignableFrom(value?.GetType())) + attribute.PropertyInfo.SetValue(result, value); + else + attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); + } + + index++; + } + + return result; } private class ArrayPropertyInfo { public PropertyInfo PropertyInfo { get; set; } = null!; public ArrayPropertyAttribute ArrayProperty { get; set; } = null!; - public Type? JsonConverterType { get; set; } + public JsonConverter? JsonConverter { get; set; } public bool DefaultDeserialization { get; set; } public Type TargetType { get; set; } = null!; } - - private class ArrayConverterInner : JsonConverter - { - private static readonly ConcurrentDictionary> _typeAttributesCache = new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary _converterOptionsCache = new ConcurrentDictionary(); - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - 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.TargetType == typeof(string)) - writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); - else if(prop.TargetType.IsEnum) - writer.WriteStringValue(EnumConverter.GetString(objValue)); - else if (prop.TargetType == typeof(bool)) - writer.WriteBooleanValue((bool)objValue); - else - writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!); - } - else - { - JsonSerializer.Serialize(writer, objValue, typeOptions ?? options); - } - } - - writer.WriteEndArray(); - } - - /// - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - return default; - - var result = Activator.CreateInstance(typeToConvert)!; - return (T)ParseObject(ref reader, result, typeToConvert, options); - } - - 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(); - var properties = type.GetProperties(); - foreach (var property in properties) - { - var att = property.GetCustomAttribute(); - if (att == null) - continue; - - attributes.Add(new ArrayPropertyInfo - { - ArrayProperty = att, - PropertyInfo = property, - DefaultDeserialization = property.GetCustomAttribute() != null, - JsonConverterType = property.GetCustomAttribute()?.ConverterType ?? property.PropertyType.GetCustomAttribute()?.ConverterType, - TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType - }); - } - - _typeAttributesCache.TryAdd(type, attributes); - return attributes; - } - - private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - throw new Exception("Not an array"); - - if (!_typeAttributesCache.TryGetValue(objectType, out var attributes)) - attributes = CacheTypeAttributes(objectType); - - int index = 0; - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndArray) - break; - - var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index); - if (!indexAttributes.Any()) - { - index++; - continue; - } - - foreach (var attribute in indexAttributes) - { - var targetType = attribute.TargetType; - object? value = null; - if (attribute.JsonConverterType != null) - { - if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions)) - { - var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!; - newOptions = new JsonSerializerOptions - { - NumberHandling = SerializerOptions.WithConverters.NumberHandling, - PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive, - Converters = { converter }, - }; - _converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions); - } - - value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions); - } - else if (attribute.DefaultDeserialization) - { - // Use default deserialization - value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters); - } - else - { - value = reader.TokenType switch - { - JsonTokenType.Null => null, - JsonTokenType.False => false, - JsonTokenType.True => true, - JsonTokenType.String => reader.GetString(), - JsonTokenType.Number => reader.GetDecimal(), - JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options), - _ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"), - }; - } - - if (targetType.IsAssignableFrom(value?.GetType())) - attribute.PropertyInfo.SetValue(result, value); - else - attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); - } - - index++; - } - - return result; - } - } } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs index 2f45135..9ada30b 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs @@ -20,8 +20,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert); - return (JsonConverter)Activator.CreateInstance(converterType)!; + return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInner(); } private class BoolConverterInner : JsonConverter diff --git a/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs index f74af7a..1954616 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs @@ -8,14 +8,14 @@ using System.Text.Json.Serialization; namespace CryptoExchange.Net.Converters.SystemTextJson { /// - /// Converter for comma seperated enum values + /// Converter for comma separated enum values /// - public class CommaSplitEnumConverter : JsonConverter> where T : Enum + public class CommaSplitEnumConverter : JsonConverter> where T: struct, Enum { /// public override IEnumerable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString(x)).ToArray() ?? new T[0])!; + return (reader.GetString()?.Split(',').Select(x => (T)EnumConverter.ParseString(x)!).ToArray() ?? []); } /// diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs index 9f3f7af..e491add 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs @@ -26,8 +26,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert); - return (JsonConverter)Activator.CreateInstance(converterType)!; + return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new DateTimeConverterInner(); } private class DateTimeConverterInner : JsonConverter diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EmptyArrayObjectConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EmptyArrayObjectConverter.cs deleted file mode 100644 index b3486f3..0000000 --- a/CryptoExchange.Net/Converters/SystemTextJson/EmptyArrayObjectConverter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace CryptoExchange.Net.Converters.SystemTextJson -{ - /// - /// Converter mapping to an object but also handles when an empty array is send - /// - /// - public class EmptyArrayObjectConverter : JsonConverter - { - private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters; - - /// - public override T? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) - { - switch (reader.TokenType) - { - case JsonTokenType.StartArray: - _ = JsonSerializer.Deserialize(ref reader, options); - return default; - case JsonTokenType.StartObject: - return JsonSerializer.Deserialize(ref reader, _defaultConverter); - }; - - return default; - } - - /// - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - => JsonSerializer.Serialize(writer, (object?)value, options); - } -} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs index 46c3ee0..f2f511e 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs @@ -11,127 +11,83 @@ using System.Text.Json.Serialization; namespace CryptoExchange.Net.Converters.SystemTextJson { + /// + /// Static EnumConverter methods + /// + public static class EnumConverter + { + /// + /// Get the enum value from a string + /// + /// String value + /// + public static T? ParseString(string value) where T : struct, Enum + => EnumConverter.ParseString(value); + + /// + /// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned + /// + /// + /// + public static string? GetString(T enumValue) where T : struct, Enum + => EnumConverter.GetString(enumValue); + + /// + /// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned + /// + /// + /// + [return: NotNullIfNotNull("enumValue")] + public static string? GetString(T? enumValue) where T : struct, Enum + => EnumConverter.GetString(enumValue); + } + /// /// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value /// - public class EnumConverter : JsonConverterFactory +#if NET5_0_OR_GREATER + public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> +#else + public class EnumConverter +#endif + : JsonConverter, INullableConverterFactory where T : struct, Enum { + private static List>? _mapping = null; private bool _warnOnMissingEntry = true; private bool _writeAsInt; - private static readonly ConcurrentDictionary>> _mapping = new(); + private NullableEnumConverter? _nullableEnumConverter = null; /// + /// ctor /// - public EnumConverter() { } + public EnumConverter() : this(false, true) + { } /// + /// ctor /// /// /// public EnumConverter(bool writeAsInt, bool warnOnMissingEntry) { - _writeAsInt = writeAsInt; _warnOnMissingEntry = warnOnMissingEntry; + _writeAsInt = writeAsInt; } - /// - public override bool CanConvert(Type typeToConvert) + internal class NullableEnumConverter : JsonConverter { - return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true; - } + private readonly EnumConverter _enumConverter; - /// - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - JsonConverter converter = (JsonConverter)Activator.CreateInstance( - typeof(EnumConverterInner<>).MakeGenericType( - new Type[] { typeToConvert }), - BindingFlags.Instance | BindingFlags.Public, - binder: null, - args: new object[] { _writeAsInt, _warnOnMissingEntry }, - culture: null)!; - - return converter; - } - - private static List> AddMapping(Type objectType) - { - var mapping = new List>(); - var enumMembers = objectType.GetMembers(); - foreach (var member in enumMembers) + public NullableEnumConverter(EnumConverter enumConverter) { - var maps = member.GetCustomAttributes(typeof(MapAttribute), false); - foreach (MapAttribute attribute in maps) - { - foreach (var value in attribute.Values) - mapping.Add(new KeyValuePair(Enum.Parse(objectType, member.Name), value)); - } + _enumConverter = enumConverter; } - _mapping.TryAdd(objectType, mapping); - return mapping; - } - - private class EnumConverterInner : JsonConverter - { - private bool _warnOnMissingEntry = true; - private bool _writeAsInt; - - public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry) - { - _warnOnMissingEntry = warnOnMissingEntry; - _writeAsInt = writeAsInt; - } - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert; - if (!_mapping.TryGetValue(enumType, out var mapping)) - mapping = AddMapping(enumType); - - var stringValue = reader.TokenType switch - { - JsonTokenType.String => reader.GetString(), - JsonTokenType.Number => reader.GetInt16().ToString(), - JsonTokenType.True => reader.GetBoolean().ToString(), - JsonTokenType.False => reader.GetBoolean().ToString(), - JsonTokenType.Null => null, - _ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType) - }; - - if (string.IsNullOrEmpty(stringValue)) - { - // Received null value - var emptyResult = GetDefaultValue(typeToConvert, enumType); - if (emptyResult != null) - // If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core). - Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo"); - - return (T?)emptyResult; - } - - if (!GetValue(enumType, mapping, stringValue!, out var result)) - { - var defaultValue = GetDefaultValue(typeToConvert, enumType); - if (string.IsNullOrWhiteSpace(stringValue)) - { - if (defaultValue != null) - // We received an empty string and have no mapping for it, and the property isn't nullable - Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo"); - } - else - { - // We received an enum value but weren't able to parse it. - if (_warnOnMissingEntry) - Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo"); - } - - return (T?)defaultValue; - } - - return (T?)result; + return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString); } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { if (value == null) { @@ -139,106 +95,173 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } else { - if (!_writeAsInt) - { - var stringValue = GetString(value.GetType(), value); - writer.WriteStringValue(stringValue); - } - else - { - writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int))); - } + _enumConverter.Write(writer, value.Value, options); } } + } - private static object? GetDefaultValue(Type objectType, Type enumType) + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString); + if (t == null) { - if (Nullable.GetUnderlyingType(objectType) != null) - return null; + if (isEmptyString) + { + // We received an empty string and have no mapping for it, and the property isn't nullable + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo"); + } + else + { + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo"); + } + return new T(); // return default value + } + else + { + return t.Value; + } + } - return Activator.CreateInstance(enumType); // return default value + private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString) + { + isEmptyString = false; + var enumType = typeof(T); + if (_mapping == null) + _mapping = AddMapping(); + + var stringValue = reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetInt16().ToString(), + JsonTokenType.True => reader.GetBoolean().ToString(), + JsonTokenType.False => reader.GetBoolean().ToString(), + JsonTokenType.Null => null, + _ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType) + }; + + if (string.IsNullOrEmpty(stringValue)) + return null; + + if (!GetValue(enumType, stringValue!, out var result)) + { + if (string.IsNullOrWhiteSpace(stringValue)) + { + isEmptyString = true; + } + else + { + // We received an enum value but weren't able to parse it. + if (_warnOnMissingEntry) + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo"); + } + + return null; } - private static bool GetValue(Type objectType, List> enumMapping, string value, out object? result) + return result; + } + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (!_writeAsInt) + { + var stringValue = GetString(value); + writer.WriteStringValue(stringValue); + } + else + { + writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int))); + } + } + + private static bool GetValue(Type objectType, string value, out T? result) + { + if (_mapping != null) { // Check for exact match first, then if not found fallback to a case insensitive match - var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); - if (mapping.Equals(default(KeyValuePair))) - mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); + var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); + if (mapping.Equals(default(KeyValuePair))) + mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); - if (!mapping.Equals(default(KeyValuePair))) + if (!mapping.Equals(default(KeyValuePair))) { result = mapping.Key; return true; } + } - if (objectType.IsDefined(typeof(FlagsAttribute))) - { - var intValue = int.Parse(value); - result = Enum.ToObject(objectType, intValue); - return true; - } + if (objectType.IsDefined(typeof(FlagsAttribute))) + { + var intValue = int.Parse(value); + result = (T)Enum.ToObject(objectType, intValue); + return true; + } - try + try + { + // If no explicit mapping is found try to parse string + result = (T)Enum.Parse(objectType, value, true); + return true; + } + catch (Exception) + { + result = default; + return false; + } + } + + private static List> AddMapping() + { + var mapping = new List>(); + var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + var enumMembers = enumType.GetFields(); + foreach (var member in enumMembers) + { + var maps = member.GetCustomAttributes(typeof(MapAttribute), false); + foreach (MapAttribute attribute in maps) { - // If no explicit mapping is found try to parse string - result = Enum.Parse(objectType, value, true); - return true; - } - catch (Exception) - { - result = default; - return false; + foreach (var value in attribute.Values) + mapping.Add(new KeyValuePair((T)Enum.Parse(enumType, member.Name), value)); } } + + _mapping = mapping; + return mapping; } /// /// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned /// - /// /// /// [return: NotNullIfNotNull("enumValue")] - public static string? GetString(T enumValue) => GetString(typeof(T), enumValue); - - /// - /// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned - /// - /// - /// - /// - [return: NotNullIfNotNull("enumValue")] - public static string? GetString(Type objectType, object? enumValue) + public static string? GetString(T? enumValue) { - objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; + if (_mapping == null) + _mapping = AddMapping(); - if (!_mapping.TryGetValue(objectType, out var mapping)) - mapping = AddMapping(objectType); - - return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); + return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); } /// /// Get the enum value from a string /// - /// Enum type /// String value /// - public static T? ParseString(string value) where T : Enum + public static T? ParseString(string value) { - var type = typeof(T); - if (!_mapping.TryGetValue(type, out var enumMapping)) - enumMapping = AddMapping(type); + var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + if (_mapping == null) + _mapping = AddMapping(); - var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); - if (mapping.Equals(default(KeyValuePair))) - mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); + var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); + if (mapping.Equals(default(KeyValuePair))) + mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); - if (!mapping.Equals(default(KeyValuePair))) - { - return (T)mapping.Key; - } + if (!mapping.Equals(default(KeyValuePair))) + return mapping.Key; try { @@ -250,5 +273,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson return default; } } + + /// + public JsonConverter CreateNullableConverter() + { + _nullableEnumConverter ??= new NullableEnumConverter(this); + return _nullableEnumConverter; + } } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs b/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs new file mode 100644 index 0000000..ea01b5a --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + internal interface INullableConverterFactory + { + JsonConverter CreateNullableConverter(); + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs b/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs new file mode 100644 index 0000000..4cd7e68 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + internal class NullableEnumConverterFactory : JsonConverterFactory + { + private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver; + private static readonly JsonSerializerOptions _options = new JsonSerializerOptions(); + + public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver) + { + _jsonTypeInfoResolver = jsonTypeInfoResolver; + } + + public override bool CanConvert(Type typeToConvert) + { + var b = Nullable.GetUnderlyingType(typeToConvert); + if (b == null) + return false; + + var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options); + if (typeInfo == null) + return false; + + return typeInfo.Converter is INullableConverterFactory; + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}"); + var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}"); + if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory) + throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}"); + + return nullConverterFactory.CreateNullableConverter(); + } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs index 542defa..d35bd98 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs @@ -5,9 +5,8 @@ using System.Text.Json; namespace CryptoExchange.Net.Converters.SystemTextJson { /// - /// + /// Converter for values which contain a nested json value /// - /// public class ObjectStringConverter : JsonConverter { /// diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs b/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs new file mode 100644 index 0000000..7596e61 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// Attribute to mark a model as json serializable. Used for AOT compilation. + /// + [AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum)] + public class SerializationModelAttribute : Attribute + { + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs b/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs index 71c867f..585c98a 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Collections.Concurrent; +using System.Text.Json; using System.Text.Json.Serialization; namespace CryptoExchange.Net.Converters.SystemTextJson @@ -8,22 +9,34 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public static class SerializerOptions { + private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + /// - /// Json serializer settings which includes the EnumConverter, DateTimeConverter, BoolConverter and DecimalConverter + /// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types /// - public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions + public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver) { - NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, - PropertyNameCaseInsensitive = false, - Converters = + if (!_cache.TryGetValue(typeResolver, out var options)) + { + options = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + Converters = { new DateTimeConverter(), - new EnumConverter(), new BoolConverter(), new DecimalConverter(), new IntConverter(), - new LongConverter() - } - }; + new LongConverter(), + new NullableEnumConverterFactory(typeResolver) + }, + TypeInfoResolver = typeResolver, + }; + _cache.TryAdd(typeResolver, options); + } + + return options; + } } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs index 45c8a86..e270d3a 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs @@ -3,6 +3,7 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; using System.Text.Json; @@ -20,7 +21,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// protected JsonDocument? _document; - private static readonly JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters; private readonly JsonSerializerOptions? _customSerializerOptions; /// @@ -32,13 +32,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public object? Underlying => throw new NotImplementedException(); - /// - /// ctor - /// - public SystemTextJsonMessageAccessor() - { - } - /// /// ctor /// @@ -48,6 +41,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif public CallResult Deserialize(Type type, MessagePath? path = null) { if (!IsJson) @@ -58,7 +55,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson try { - var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions); + var result = _document.Deserialize(type, _customSerializerOptions); return new CallResult(result!); } catch (JsonException ex) @@ -74,6 +71,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif public CallResult Deserialize(MessagePath? path = null) { if (_document == null) @@ -81,7 +82,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson try { - var result = _document.Deserialize(_customSerializerOptions ?? _serializerOptions); + var result = _document.Deserialize(_customSerializerOptions); return new CallResult(result!); } catch (JsonException ex) @@ -132,6 +133,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif public T? GetValue(MessagePath path) { if (!IsJson) @@ -145,7 +150,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { try { - return value.Value.Deserialize(_customSerializerOptions ?? _serializerOptions); + return value.Value.Deserialize(_customSerializerOptions); } catch { } @@ -158,7 +163,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson return (T)(object)value.Value.GetInt64().ToString(); } - return value.Value.Deserialize(); + return value.Value.Deserialize(_customSerializerOptions); } /// @@ -240,13 +245,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public override bool OriginalDataAvailable => _stream?.CanSeek == true; - /// - /// ctor - /// - public SystemTextJsonStreamMessageAccessor(): base() - { - } - /// /// ctor /// @@ -317,13 +315,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { private ReadOnlyMemory _bytes; - /// - /// ctor - /// - public SystemTextJsonByteMessageAccessor() : base() - { - } - /// /// ctor /// diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs index 8c4cdf7..ab04034 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs @@ -1,12 +1,29 @@ using CryptoExchange.Net.Interfaces; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace CryptoExchange.Net.Converters.SystemTextJson { /// public class SystemTextJsonMessageSerializer : IMessageSerializer { + private readonly JsonSerializerContext _options; + + /// + /// ctor + /// + public SystemTextJsonMessageSerializer(JsonSerializerContext options) + { + _options = options; + } + /// - public string Serialize(object message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters); +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] +#endif + public string Serialize(T message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters(_options)); } } diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 3f55424..bb0957e 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -1,6 +1,6 @@ - netstandard2.0;netstandard2.1;net9.0 + netstandard2.0;netstandard2.1;net8.0;net9.0 CryptoExchange.Net @@ -27,6 +27,9 @@ + + true + true true diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 4f72a1d..0352f88 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -10,6 +10,9 @@ using CryptoExchange.Net.Objects; using System.Globalization; using Microsoft.Extensions.DependencyInjection; using CryptoExchange.Net.SharedApis; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json; +using System.Text.Json.Serialization; namespace CryptoExchange.Net { diff --git a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs index 61ffecb..544cf93 100644 --- a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs +++ b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs @@ -10,6 +10,6 @@ /// /// /// - string Serialize(object message); + string Serialize(T message); } } diff --git a/CryptoExchange.Net/Objects/ParameterCollection.cs b/CryptoExchange.Net/Objects/ParameterCollection.cs index 599067e..1512995 100644 --- a/CryptoExchange.Net/Objects/ParameterCollection.cs +++ b/CryptoExchange.Net/Objects/ParameterCollection.cs @@ -2,6 +2,7 @@ using CryptoExchange.Net.Converters.SystemTextJson; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; @@ -173,11 +174,14 @@ namespace CryptoExchange.Net.Objects /// /// Add an enum value as the string value as mapped using the /// - /// - /// +#if NET5_0_OR_GREATER + public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) +#else public void AddEnum(string key, T value) +#endif + where T : struct, Enum { - Add(key, EnumConverter.GetString(value)!); + Add(key, EnumConverter.GetString(value)!); } /// @@ -185,9 +189,14 @@ namespace CryptoExchange.Net.Objects /// /// /// +#if NET5_0_OR_GREATER + public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) +#else public void AddEnumAsInt(string key, T value) +#endif + where T : struct, Enum { - var stringVal = EnumConverter.GetString(value)!; + var stringVal = EnumConverter.GetString(value)!; Add(key, int.Parse(stringVal)!); } @@ -196,22 +205,30 @@ namespace CryptoExchange.Net.Objects /// /// /// +#if NET5_0_OR_GREATER + public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) +#else public void AddOptionalEnum(string key, T? value) +#endif + where T : struct, Enum { if (value != null) - Add(key, EnumConverter.GetString(value)); + Add(key, EnumConverter.GetString(value)); } /// /// Add an enum value as the string value as mapped using the . Not added if value is null /// - /// - /// +#if NET5_0_OR_GREATER + public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) +#else public void AddOptionalEnumAsInt(string key, T? value) +#endif + where T : struct, Enum { if (value != null) { - var stringVal = EnumConverter.GetString(value); + var stringVal = EnumConverter.GetString(value); Add(key, int.Parse(stringVal)); } } diff --git a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs index 3adbdb3..46c27c7 100644 --- a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs +++ b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs @@ -98,7 +98,7 @@ namespace CryptoExchange.Net.Testing.Comparers var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter)) + if (jsonConverter != typeof(ArrayConverter<,>)) // Not array converter? continue; @@ -237,7 +237,7 @@ namespace CryptoExchange.Net.Testing.Comparers throw new Exception("Enumeration not moved; incorrect amount of results?"); var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true); - if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter)) + if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter<,>)) // Custom converter for the type, skip continue; @@ -257,7 +257,7 @@ namespace CryptoExchange.Net.Testing.Comparers var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter)) + if (jsonConverter != typeof(ArrayConverter<,>)) // Not array converter? continue; @@ -323,7 +323,7 @@ namespace CryptoExchange.Net.Testing.Comparers var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter)) + if (jsonConverter != typeof(ArrayConverter<,>)) // Not array converter? continue;