1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-07 16:06:15 +00:00
This commit is contained in:
Jkorf 2025-03-05 12:59:42 +01:00
commit 89bd091848
22 changed files with 592 additions and 484 deletions

View File

@ -235,6 +235,7 @@ namespace CryptoExchange.Net.UnitTests
} }
[JsonConverter(typeof(EnumConverter))] [JsonConverter(typeof(EnumConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(CryptoExchange.Net.Converters.SystemTextJson.EnumConverter<TestEnum>))]
public enum TestEnum public enum TestEnum
{ {
[Map("1")] [Map("1")]

View File

@ -146,7 +146,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected) public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
{ {
var val = value == null ? "null" : $"\"{value}\""; var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}"); var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected); Assert.That(output.Value == expected);
} }
@ -171,8 +171,8 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("three", TestEnum.Three)] [TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)] [TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)] [TestCase("four", TestEnum.Four)]
[TestCase("Four1", TestEnum.One)] [TestCase("Four1", null)]
[TestCase(null, TestEnum.One)] [TestCase(null, null)]
public void TestEnumConverterParseStringTests(string value, TestEnum? expected) public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
{ {
var result = EnumConverter.ParseString<TestEnum>(value); var result = EnumConverter.ParseString<TestEnum>(value);
@ -194,7 +194,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestBoolConverter(string value, bool? expected) public void TestBoolConverter(string value, bool? expected)
{ {
var val = value == null ? "null" : $"\"{value}\""; var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}"); var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected); Assert.That(output.Value == expected);
} }
@ -213,7 +213,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestBoolConverterNotNullable(string value, bool expected) public void TestBoolConverterNotNullable(string value, bool expected)
{ {
var val = value == null ? "null" : $"\"{value}\""; var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}"); var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected); Assert.That(output.Value == expected);
} }
@ -265,7 +265,11 @@ namespace CryptoExchange.Net.UnitTests
Prop31 = 4, Prop31 = 4,
Prop32 = "789" Prop32 = "789"
}, },
Prop7 = TestEnum.Two Prop7 = TestEnum.Two,
TestInternal = new Test
{
Prop1 = 10
}
}; };
var serialized = JsonSerializer.Serialize(data); 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.Prop31, Is.EqualTo(4));
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789")); Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two)); 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 public class STJEnumObject
{ {
[JsonConverter(typeof(EnumConverter))]
public TestEnum? Value { get; set; } public TestEnum? Value { get; set; }
} }
public class NotNullableSTJEnumObject public class NotNullableSTJEnumObject
{ {
[JsonConverter(typeof(EnumConverter))]
public TestEnum Value { get; set; } public TestEnum Value { get; set; }
} }
public class STJBoolObject public class STJBoolObject
{ {
[JsonConverter(typeof(BoolConverter))]
public bool? Value { get; set; } public bool? Value { get; set; }
} }
public class NotNullableSTJBoolObject public class NotNullableSTJBoolObject
{ {
[JsonConverter(typeof(BoolConverter))]
public bool Value { get; set; } public bool Value { get; set; }
} }
[JsonConverter(typeof(ArrayConverter))] [JsonConverter(typeof(ArrayConverter<Test, SerializationContext>))]
record Test record Test
{ {
[ArrayProperty(0)] [ArrayProperty(0)]
@ -339,11 +340,13 @@ namespace CryptoExchange.Net.UnitTests
public Test2 Prop5 { get; set; } public Test2 Prop5 { get; set; }
[ArrayProperty(5)] [ArrayProperty(5)]
public Test3 Prop6 { get; set; } public Test3 Prop6 { get; set; }
[ArrayProperty(6), JsonConverter(typeof(EnumConverter))] [ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
public TestEnum? Prop7 { get; set; } public TestEnum? Prop7 { get; set; }
[ArrayProperty(7)]
public Test TestInternal { get; set; }
} }
[JsonConverter(typeof(ArrayConverter))] [JsonConverter(typeof(ArrayConverter<Test2, SerializationContext>))]
record Test2 record Test2
{ {
[ArrayProperty(0)] [ArrayProperty(0)]
@ -359,4 +362,17 @@ namespace CryptoExchange.Net.UnitTests
[JsonPropertyName("prop32")] [JsonPropertyName("prop32")]
public string Prop32 { get; set; } 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
{
}
} }

View File

@ -49,26 +49,5 @@ namespace CryptoExchange.Net.Authentication
{ {
return new ApiCredentials(Key, Secret, CredentialType); return new ApiCredentials(Key, Secret, CredentialType);
} }
/// <summary>
/// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret
/// </summary>
/// <param name="inputStream">The stream containing the json data</param>
/// <param name="identifierKey">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'.</param>
/// <param name="identifierSecret">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'.</param>
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<string>(MessagePath.Get().Property(identifierKey ?? "apiKey"));
var secret = accessor.GetValue<string>(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);
}
} }
} }

View File

@ -7,6 +7,6 @@ namespace CryptoExchange.Net.Converters.JsonNet
public class JsonNetMessageSerializer : IMessageSerializer public class JsonNetMessageSerializer : IMessageSerializer
{ {
/// <inheritdoc /> /// <inheritdoc />
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None); public string Serialize<T>(T message) => JsonConvert.SerializeObject(message, Formatting.None);
} }
} }

View File

@ -7,6 +7,7 @@ using System.Text.Json.Serialization;
using System.Text.Json; using System.Text.Json;
using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Attributes;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
@ -14,32 +15,20 @@ 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 /// 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 /// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary> /// </summary>
public class ArrayConverter : JsonConverterFactory #if NET5_0_OR_GREATER
{ public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TContext> : JsonConverter<T> where T : new() where TContext: JsonSerializerContext
/// <inheritdoc /> # else
public override bool CanConvert(Type typeToConvert) => true; public class ArrayConverter<T, TContext> : JsonConverter<T> where T : new() where TContext: JsonSerializerContext
#endif
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType)!;
}
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public Type? JsonConverterType { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
}
private class ArrayConverterInner<T> : JsonConverter<T>
{ {
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>(); private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>(); private static readonly ConcurrentDictionary<JsonConverter, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<JsonConverter, JsonSerializerOptions>();
/// <inheritdoc />
#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) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{ {
if (value == null) if (value == null)
@ -50,7 +39,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteStartArray(); writer.WriteStartArray();
var valueType = value.GetType(); var valueType = typeof(T);
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes)) if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
typeAttributes = CacheTypeAttributes(valueType); typeAttributes = CacheTypeAttributes(valueType);
@ -77,20 +66,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
JsonSerializerOptions? typeOptions = null; JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverterType != null) if (prop.JsonConverter != null)
{ {
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!; typeOptions = new JsonSerializerOptions
typeOptions = new JsonSerializerOptions(); {
typeOptions.Converters.Clear(); NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
typeOptions.Converters.Add(converter); PropertyNameCaseInsensitive = false,
TypeInfoResolver = (TContext)Activator.CreateInstance(typeof(TContext))!,
};
typeOptions.Converters.Add(prop.JsonConverter);
} }
if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType)) if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
{ {
if (prop.TargetType == typeof(string)) if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if(prop.TargetType.IsEnum)
writer.WriteStringValue(EnumConverter.GetString(objValue));
else if (prop.TargetType == typeof(bool)) else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue); writer.WriteBooleanValue((bool)objValue);
else else
@ -111,8 +101,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null) if (reader.TokenType == JsonTokenType.Null)
return default; return default;
var result = Activator.CreateInstance(typeToConvert)!; var result = Activator.CreateInstance(typeof(T))!;
return (T)ParseObject(ref reader, result, typeToConvert, options); return (T)ParseObject(ref reader, result, typeof(T), options);
} }
private static bool IsSimple(Type type) private static bool IsSimple(Type type)
@ -128,7 +118,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| type == typeof(decimal); || 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<ArrayPropertyInfo> CacheTypeAttributes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
#else
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type) private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
#endif
{ {
var attributes = new List<ArrayPropertyInfo>(); var attributes = new List<ArrayPropertyInfo>();
var properties = type.GetProperties(); var properties = type.GetProperties();
@ -138,12 +134,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (att == null) if (att == null)
continue; continue;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
attributes.Add(new ArrayPropertyInfo attributes.Add(new ArrayPropertyInfo
{ {
ArrayProperty = att, ArrayProperty = att,
PropertyInfo = property, PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null, DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType, JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
}); });
} }
@ -152,7 +149,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return 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) private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
#endif
{ {
if (reader.TokenType != JsonTokenType.StartArray) if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("Not an array"); throw new Exception("Not an array");
@ -177,18 +181,18 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
var targetType = attribute.TargetType; var targetType = attribute.TargetType;
object? value = null; object? value = null;
if (attribute.JsonConverterType != null) if (attribute.JsonConverter != null)
{ {
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions)) if (!_converterOptionsCache.TryGetValue(attribute.JsonConverter, out var newOptions))
{ {
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
newOptions = new JsonSerializerOptions newOptions = new JsonSerializerOptions
{ {
NumberHandling = SerializerOptions.WithConverters.NumberHandling, NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive, PropertyNameCaseInsensitive = false,
Converters = { converter }, Converters = { attribute.JsonConverter },
TypeInfoResolver = options.TypeInfoResolver,
}; };
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions); _converterOptionsCache.TryAdd(attribute.JsonConverter, newOptions);
} }
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions); value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
@ -196,7 +200,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
else if (attribute.DefaultDeserialization) else if (attribute.DefaultDeserialization)
{ {
// Use default deserialization // Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters); value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters((TContext)Activator.CreateInstance(typeof(TContext))!));
} }
else else
{ {
@ -223,6 +227,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return result; return result;
} }
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public JsonConverter? JsonConverter { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
} }
} }
} }

View File

@ -20,8 +20,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert); return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class BoolConverterInner<T> : JsonConverter<T> private class BoolConverterInner<T> : JsonConverter<T>

View File

@ -8,14 +8,14 @@ using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
/// <summary> /// <summary>
/// Converter for comma seperated enum values /// Converter for comma separated enum values
/// </summary> /// </summary>
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T: struct, Enum
{ {
/// <inheritdoc /> /// <inheritdoc />
public override IEnumerable<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override IEnumerable<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString<T>(x)).ToArray() ?? new T[0])!; return (reader.GetString()?.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? []);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -26,8 +26,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert); return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class DateTimeConverterInner<T> : JsonConverter<T> private class DateTimeConverterInner<T> : JsonConverter<T>

View File

@ -1,37 +0,0 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter mapping to an object but also handles when an empty array is send
/// </summary>
/// <typeparam name="T"></typeparam>
public class EmptyArrayObjectConverter<T> : JsonConverter<T>
{
private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters;
/// <inheritdoc />
public override T? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.StartArray:
_ = JsonSerializer.Deserialize<object[]>(ref reader, options);
return default;
case JsonTokenType.StartObject:
return JsonSerializer.Deserialize<T>(ref reader, _defaultConverter);
};
return default;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, (object?)value, options);
}
}

View File

@ -11,82 +11,124 @@ using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
/// <summary>
/// Static EnumConverter methods
/// </summary>
public static class EnumConverter
{
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <param name="value">String value</param>
/// <returns></returns>
public static T? ParseString<T>(string value) where T : struct, Enum
=> EnumConverter<T>.ParseString(value);
/// <summary>
/// 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
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
public static string? GetString<T>(T enumValue) where T : struct, Enum
=> EnumConverter<T>.GetString(enumValue);
/// <summary>
/// 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
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString<T>(T? enumValue) where T : struct, Enum
=> EnumConverter<T>.GetString(enumValue);
}
/// <summary> /// <summary>
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value /// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
/// </summary> /// </summary>
public class EnumConverter : JsonConverterFactory #if NET5_0_OR_GREATER
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
#else
public class EnumConverter<T>
#endif
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
{ {
private static List<KeyValuePair<T, string>>? _mapping = null;
private bool _warnOnMissingEntry = true; private bool _warnOnMissingEntry = true;
private bool _writeAsInt; private bool _writeAsInt;
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new(); private NullableEnumConverter? _nullableEnumConverter = null;
/// <summary> /// <summary>
/// ctor
/// </summary> /// </summary>
public EnumConverter() { } public EnumConverter() : this(false, true)
{ }
/// <summary> /// <summary>
/// ctor
/// </summary> /// </summary>
/// <param name="writeAsInt"></param> /// <param name="writeAsInt"></param>
/// <param name="warnOnMissingEntry"></param> /// <param name="warnOnMissingEntry"></param>
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry) public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
{
_writeAsInt = writeAsInt;
_warnOnMissingEntry = warnOnMissingEntry;
}
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;
}
/// <inheritdoc />
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<KeyValuePair<object, string>> AddMapping(Type objectType)
{
var mapping = new List<KeyValuePair<object, string>>();
var enumMembers = objectType.GetMembers();
foreach (var member in enumMembers)
{
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
}
}
_mapping.TryAdd(objectType, mapping);
return mapping;
}
private class EnumConverterInner<T> : JsonConverter<T>
{
private bool _warnOnMissingEntry = true;
private bool _writeAsInt;
public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry)
{ {
_warnOnMissingEntry = warnOnMissingEntry; _warnOnMissingEntry = warnOnMissingEntry;
_writeAsInt = writeAsInt; _writeAsInt = writeAsInt;
} }
internal class NullableEnumConverter : JsonConverter<T?>
{
private readonly EnumConverter<T> _enumConverter;
public NullableEnumConverter(EnumConverter<T> enumConverter)
{
_enumConverter = enumConverter;
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert; return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
if (!_mapping.TryGetValue(enumType, out var mapping)) }
mapping = AddMapping(enumType);
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
}
else
{
_enumConverter.Write(writer, value.Value, options);
}
}
}
/// <inheritdoc />
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 (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;
}
}
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 var stringValue = reader.TokenType switch
{ {
@ -99,49 +141,33 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}; };
if (string.IsNullOrEmpty(stringValue)) if (string.IsNullOrEmpty(stringValue))
{ return null;
// 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, stringValue!, out var result))
}
if (!GetValue(enumType, mapping, stringValue!, out var result))
{ {
var defaultValue = GetDefaultValue(typeToConvert, enumType);
if (string.IsNullOrWhiteSpace(stringValue)) if (string.IsNullOrWhiteSpace(stringValue))
{ {
if (defaultValue != null) isEmptyString = true;
// 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 else
{ {
// We received an enum value but weren't able to parse it. // We received an enum value but weren't able to parse it.
if (_warnOnMissingEntry) 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"); 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 null;
} }
return (T?)result; return result;
} }
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
}
else
{ {
if (!_writeAsInt) if (!_writeAsInt)
{ {
var stringValue = GetString(value.GetType(), value); var stringValue = GetString(value);
writer.WriteStringValue(stringValue); writer.WriteStringValue(stringValue);
} }
else else
@ -149,40 +175,34 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int))); writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int)));
} }
} }
}
private static object? GetDefaultValue(Type objectType, Type enumType) private static bool GetValue(Type objectType, string value, out T? result)
{ {
if (Nullable.GetUnderlyingType(objectType) != null) if (_mapping != null)
return null;
return Activator.CreateInstance(enumType); // return default value
}
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
{ {
// Check for exact match first, then if not found fallback to a case insensitive match // 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)); var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<object, string>))) if (mapping.Equals(default(KeyValuePair<T, string>)))
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<object, string>))) if (!mapping.Equals(default(KeyValuePair<T, string>)))
{ {
result = mapping.Key; result = mapping.Key;
return true; return true;
} }
}
if (objectType.IsDefined(typeof(FlagsAttribute))) if (objectType.IsDefined(typeof(FlagsAttribute)))
{ {
var intValue = int.Parse(value); var intValue = int.Parse(value);
result = Enum.ToObject(objectType, intValue); result = (T)Enum.ToObject(objectType, intValue);
return true; return true;
} }
try try
{ {
// If no explicit mapping is found try to parse string // If no explicit mapping is found try to parse string
result = Enum.Parse(objectType, value, true); result = (T)Enum.Parse(objectType, value, true);
return true; return true;
} }
catch (Exception) catch (Exception)
@ -191,54 +211,57 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return false; return false;
} }
} }
private static List<KeyValuePair<T, string>> AddMapping()
{
var mapping = new List<KeyValuePair<T, string>>();
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)
{
foreach (var value in attribute.Values)
mapping.Add(new KeyValuePair<T, string>((T)Enum.Parse(enumType, member.Name), value));
}
}
_mapping = mapping;
return mapping;
} }
/// <summary> /// <summary>
/// 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 /// 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
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="enumValue"></param> /// <param name="enumValue"></param>
/// <returns></returns> /// <returns></returns>
[return: NotNullIfNotNull("enumValue")] [return: NotNullIfNotNull("enumValue")]
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue); public static string? GetString(T? enumValue)
/// <summary>
/// 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
/// </summary>
/// <param name="objectType"></param>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString(Type objectType, object? enumValue)
{ {
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; if (_mapping == null)
_mapping = AddMapping();
if (!_mapping.TryGetValue(objectType, out var mapping)) return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
mapping = AddMapping(objectType);
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
} }
/// <summary> /// <summary>
/// Get the enum value from a string /// Get the enum value from a string
/// </summary> /// </summary>
/// <typeparam name="T">Enum type</typeparam>
/// <param name="value">String value</param> /// <param name="value">String value</param>
/// <returns></returns> /// <returns></returns>
public static T? ParseString<T>(string value) where T : Enum public static T? ParseString(string value)
{ {
var type = typeof(T); var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (!_mapping.TryGetValue(type, out var enumMapping)) if (_mapping == null)
enumMapping = AddMapping(type); _mapping = AddMapping();
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<object, string>))) if (mapping.Equals(default(KeyValuePair<T, string>)))
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<object, string>))) if (!mapping.Equals(default(KeyValuePair<T, string>)))
{ return mapping.Key;
return (T)mapping.Key;
}
try try
{ {
@ -250,5 +273,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return default; return default;
} }
} }
/// <inheritdoc />
public JsonConverter CreateNullableConverter()
{
_nullableEnumConverter ??= new NullableEnumConverter(this);
return _nullableEnumConverter;
}
} }
} }

View File

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal interface INullableConverterFactory
{
JsonConverter CreateNullableConverter();
}
}

View File

@ -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();
}
}
}

View File

@ -5,9 +5,8 @@ using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
/// <summary> /// <summary>
/// /// Converter for values which contain a nested json value
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam>
public class ObjectStringConverter<T> : JsonConverter<T> public class ObjectStringConverter<T> : JsonConverter<T>
{ {
/// <inheritdoc /> /// <inheritdoc />

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Attribute to mark a model as json serializable. Used for AOT compilation.
/// </summary>
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum)]
public class SerializationModelAttribute : Attribute
{
}
}

View File

@ -1,4 +1,5 @@
using System.Text.Json; using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson
@ -8,22 +9,34 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// </summary> /// </summary>
public static class SerializerOptions public static class SerializerOptions
{ {
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
/// <summary> /// <summary>
/// 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
/// </summary> /// </summary>
public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver)
{
if (!_cache.TryGetValue(typeResolver, out var options))
{
options = new JsonSerializerOptions
{ {
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
Converters = Converters =
{ {
new DateTimeConverter(), new DateTimeConverter(),
new EnumConverter(),
new BoolConverter(), new BoolConverter(),
new DecimalConverter(), new DecimalConverter(),
new IntConverter(), new IntConverter(),
new LongConverter() new LongConverter(),
} new NullableEnumConverterFactory(typeResolver)
},
TypeInfoResolver = typeResolver,
}; };
_cache.TryAdd(typeResolver, options);
}
return options;
}
} }
} }

View File

@ -3,6 +3,7 @@ using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -20,7 +21,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// </summary> /// </summary>
protected JsonDocument? _document; protected JsonDocument? _document;
private static readonly JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
private readonly JsonSerializerOptions? _customSerializerOptions; private readonly JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc /> /// <inheritdoc />
@ -32,13 +32,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public object? Underlying => throw new NotImplementedException(); public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor()
{
}
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@ -48,6 +41,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
/// <inheritdoc /> /// <inheritdoc />
#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<object> Deserialize(Type type, MessagePath? path = null) public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{ {
if (!IsJson) if (!IsJson)
@ -58,7 +55,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions); var result = _document.Deserialize(type, _customSerializerOptions);
return new CallResult<object>(result!); return new CallResult<object>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@ -74,6 +71,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
/// <inheritdoc /> /// <inheritdoc />
#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<T> Deserialize<T>(MessagePath? path = null) public CallResult<T> Deserialize<T>(MessagePath? path = null)
{ {
if (_document == null) if (_document == null)
@ -81,7 +82,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions); var result = _document.Deserialize<T>(_customSerializerOptions);
return new CallResult<T>(result!); return new CallResult<T>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@ -132,6 +133,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
/// <inheritdoc /> /// <inheritdoc />
#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<T>(MessagePath path) public T? GetValue<T>(MessagePath path)
{ {
if (!IsJson) if (!IsJson)
@ -145,7 +150,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
try try
{ {
return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions); return value.Value.Deserialize<T>(_customSerializerOptions);
} }
catch { } catch { }
@ -158,7 +163,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return (T)(object)value.Value.GetInt64().ToString(); return (T)(object)value.Value.GetInt64().ToString();
} }
return value.Value.Deserialize<T>(); return value.Value.Deserialize<T>(_customSerializerOptions);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -240,13 +245,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true; public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(): base()
{
}
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@ -317,13 +315,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
private ReadOnlyMemory<byte> _bytes; private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor() : base()
{
}
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>

View File

@ -1,12 +1,29 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
/// <inheritdoc /> /// <inheritdoc />
public class SystemTextJsonMessageSerializer : IMessageSerializer public class SystemTextJsonMessageSerializer : IMessageSerializer
{ {
private readonly JsonSerializerContext _options;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageSerializer(JsonSerializerContext options)
{
_options = options;
}
/// <inheritdoc /> /// <inheritdoc />
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>(T message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters(_options));
} }
} }

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
@ -27,6 +27,9 @@
<None Include="Icon\icon.png" Pack="true" PackagePath="\" /> <None Include="Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="..\README.md" Pack="true" PackagePath="\" /> <None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="AOT" Condition=" '$(TargetFramework)' == 'NET8_0' Or '$(TargetFramework)' == 'NET9_0' ">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'"> <PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>

View File

@ -10,6 +10,9 @@ using CryptoExchange.Net.Objects;
using System.Globalization; using System.Globalization;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {

View File

@ -10,6 +10,6 @@
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
/// <returns></returns> /// <returns></returns>
string Serialize(object message); string Serialize<T>(T message);
} }
} }

View File

@ -2,6 +2,7 @@
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -173,11 +174,14 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" /> /// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
/// </summary> /// </summary>
/// <param name="key"></param> #if NET5_0_OR_GREATER
/// <param name="value"></param> public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
#else
public void AddEnum<T>(string key, T value) public void AddEnum<T>(string key, T value)
#endif
where T : struct, Enum
{ {
Add(key, EnumConverter.GetString(value)!); Add(key, EnumConverter<T>.GetString(value)!);
} }
/// <summary> /// <summary>
@ -185,9 +189,14 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="value"></param> /// <param name="value"></param>
#if NET5_0_OR_GREATER
public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
#else
public void AddEnumAsInt<T>(string key, T value) public void AddEnumAsInt<T>(string key, T value)
#endif
where T : struct, Enum
{ {
var stringVal = EnumConverter.GetString(value)!; var stringVal = EnumConverter<T>.GetString(value)!;
Add(key, int.Parse(stringVal)!); Add(key, int.Parse(stringVal)!);
} }
@ -196,22 +205,30 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="value"></param> /// <param name="value"></param>
#if NET5_0_OR_GREATER
public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
#else
public void AddOptionalEnum<T>(string key, T? value) public void AddOptionalEnum<T>(string key, T? value)
#endif
where T : struct, Enum
{ {
if (value != null) if (value != null)
Add(key, EnumConverter.GetString(value)); Add(key, EnumConverter<T>.GetString(value));
} }
/// <summary> /// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null /// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
/// </summary> /// </summary>
/// <param name="key"></param> #if NET5_0_OR_GREATER
/// <param name="value"></param> public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
#else
public void AddOptionalEnumAsInt<T>(string key, T? value) public void AddOptionalEnumAsInt<T>(string key, T? value)
#endif
where T : struct, Enum
{ {
if (value != null) if (value != null)
{ {
var stringVal = EnumConverter.GetString(value); var stringVal = EnumConverter<T>.GetString(value);
Add(key, int.Parse(stringVal)); Add(key, int.Parse(stringVal));
} }
} }

View File

@ -98,7 +98,7 @@ namespace CryptoExchange.Net.Testing.Comparers
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault())); var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
if (jsonConverter != typeof(ArrayConverter)) if (jsonConverter != typeof(ArrayConverter<,>))
// Not array converter? // Not array converter?
continue; continue;
@ -237,7 +237,7 @@ namespace CryptoExchange.Net.Testing.Comparers
throw new Exception("Enumeration not moved; incorrect amount of results?"); throw new Exception("Enumeration not moved; incorrect amount of results?");
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true); 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 // Custom converter for the type, skip
continue; continue;
@ -257,7 +257,7 @@ namespace CryptoExchange.Net.Testing.Comparers
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault())); var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
if (jsonConverter != typeof(ArrayConverter)) if (jsonConverter != typeof(ArrayConverter<,>))
// Not array converter? // Not array converter?
continue; continue;
@ -323,7 +323,7 @@ namespace CryptoExchange.Net.Testing.Comparers
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault())); var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
if (jsonConverter != typeof(ArrayConverter)) if (jsonConverter != typeof(ArrayConverter<,>))
// Not array converter? // Not array converter?
continue; continue;