mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2026-04-07 10:11:10 +00:00
Updated EnumConverter to remove allocation in best case path
This commit is contained in:
parent
8f2adaabe2
commit
406ae08c17
@ -82,13 +82,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
|
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
|
||||||
private static FrozenDictionary<T, string>? _mappingToString = null;
|
private static FrozenDictionary<T, string>? _mappingToString = null;
|
||||||
|
|
||||||
|
private static bool RunOptimistic => true;
|
||||||
#else
|
#else
|
||||||
private static List<EnumMapping>? _mappingToEnum = null;
|
private static List<EnumMapping>? _mappingToEnum = null;
|
||||||
private static Dictionary<T, string>? _mappingToString = null;
|
private static Dictionary<T, string>? _mappingToString = null;
|
||||||
|
|
||||||
|
// In NetStandard the `ValueTextEquals` method used is slower than just string comparing
|
||||||
|
// so only bother in newer frameworks
|
||||||
|
private static bool RunOptimistic => false;
|
||||||
#endif
|
#endif
|
||||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||||
|
|
||||||
|
private static Type _enumType = typeof(T);
|
||||||
private static T? _undefinedEnumValue;
|
private static T? _undefinedEnumValue;
|
||||||
|
private static bool _hasFlagsAttribute = _enumType.IsDefined(typeof(FlagsAttribute));
|
||||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||||
|
|
||||||
internal class NullableEnumConverter : JsonConverter<T?>
|
internal class NullableEnumConverter : JsonConverter<T?>
|
||||||
@ -153,10 +161,16 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull)
|
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull)
|
||||||
{
|
{
|
||||||
isEmptyStringOrNull = false;
|
isEmptyStringOrNull = false;
|
||||||
var enumType = typeof(T);
|
|
||||||
if (_mappingToEnum == null)
|
if (_mappingToEnum == null)
|
||||||
CreateMapping();
|
CreateMapping();
|
||||||
|
|
||||||
|
if (RunOptimistic)
|
||||||
|
{
|
||||||
|
var resultOptimistic = GetValueOptimistic(ref reader);
|
||||||
|
if (resultOptimistic != null)
|
||||||
|
return resultOptimistic.Value;
|
||||||
|
}
|
||||||
|
|
||||||
var stringValue = reader.TokenType switch
|
var stringValue = reader.TokenType switch
|
||||||
{
|
{
|
||||||
JsonTokenType.String => reader.GetString(),
|
JsonTokenType.String => reader.GetString(),
|
||||||
@ -173,8 +187,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GetValue(enumType, stringValue, out var result))
|
if (!GetValue(stringValue, out var result))
|
||||||
{
|
{
|
||||||
|
// Note: checking this here and before the GetValue seems redundant but it allows enum mapping for empty strings
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
{
|
{
|
||||||
isEmptyStringOrNull = true;
|
isEmptyStringOrNull = true;
|
||||||
@ -185,13 +200,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (!_unknownValuesWarned.Contains(stringValue))
|
if (!_unknownValuesWarned.Contains(stringValue))
|
||||||
{
|
{
|
||||||
_unknownValuesWarned.Add(stringValue!);
|
_unknownValuesWarned.Add(stringValue!);
|
||||||
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should added please open an issue on the Github repo");
|
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should be added please open an issue on the Github repo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RunOptimistic)
|
||||||
|
{
|
||||||
|
if (!_unknownValuesWarned.Contains(stringValue))
|
||||||
|
{
|
||||||
|
_unknownValuesWarned.Add(stringValue!);
|
||||||
|
LibraryHelpers.StaticLogger?.LogTrace($"Enum mapping sub-optimal. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,11 +226,32 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
writer.WriteStringValue(stringValue);
|
writer.WriteStringValue(stringValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool GetValue(Type objectType, string value, out T? result)
|
/// <summary>
|
||||||
|
/// Try to get the enum value based on the string value using the Utf8JsonReader's ValueTextEquals method.
|
||||||
|
/// This is an optimization to avoid string allocations when possible, but can only match case insensitively
|
||||||
|
/// </summary>
|
||||||
|
private static T? GetValueOptimistic(ref Utf8JsonReader reader)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.String)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
foreach (var item in _mappingToEnum!)
|
||||||
|
{
|
||||||
|
if (reader.ValueTextEquals(item.StringValue))
|
||||||
|
return item.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetValue(string value, out T? result)
|
||||||
{
|
{
|
||||||
if (_mappingToEnum != null)
|
if (_mappingToEnum != null)
|
||||||
{
|
{
|
||||||
EnumMapping? mapping = null;
|
EnumMapping? mapping = null;
|
||||||
|
// If we tried the optimistic path first we already know its not case match
|
||||||
|
if (!RunOptimistic)
|
||||||
|
{
|
||||||
// Try match on full equals
|
// Try match on full equals
|
||||||
foreach (var item in _mappingToEnum)
|
foreach (var item in _mappingToEnum)
|
||||||
{
|
{
|
||||||
@ -216,6 +261,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If not found, try matching ignoring case
|
// If not found, try matching ignoring case
|
||||||
if (mapping == null)
|
if (mapping == null)
|
||||||
@ -237,10 +283,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
if (_hasFlagsAttribute)
|
||||||
{
|
{
|
||||||
var intValue = int.Parse(value);
|
var intValue = int.Parse(value);
|
||||||
result = (T)Enum.ToObject(objectType, intValue);
|
result = (T)Enum.ToObject(_enumType, intValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,8 +308,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
// If no explicit mapping is found try to parse string
|
||||||
result = (T)Enum.Parse(objectType, value, true);
|
#if NET8_0_OR_GREATER
|
||||||
if (!Enum.IsDefined(objectType, result))
|
result = Enum.Parse<T>(value, true);
|
||||||
|
#else
|
||||||
|
result = (T)Enum.Parse(_enumType, value, true);
|
||||||
|
#endif
|
||||||
|
if (!Enum.IsDefined(_enumType, result))
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
@ -280,11 +330,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
private static void CreateMapping()
|
private static void CreateMapping()
|
||||||
{
|
{
|
||||||
var mappingToEnum = new List<EnumMapping>();
|
var mappingStringToEnum = new List<EnumMapping>();
|
||||||
var mappingToString = new Dictionary<T, string>();
|
var mappingEnumToString = new Dictionary<T, string>();
|
||||||
|
|
||||||
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
var enumMembers = _enumType.GetFields();
|
||||||
var enumMembers = enumType.GetFields();
|
|
||||||
foreach (var member in enumMembers)
|
foreach (var member in enumMembers)
|
||||||
{
|
{
|
||||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||||
@ -292,23 +341,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
foreach (var value in attribute.Values)
|
foreach (var value in attribute.Values)
|
||||||
{
|
{
|
||||||
var enumVal = (T)Enum.Parse(enumType, member.Name);
|
#if NET8_0_OR_GREATER
|
||||||
mappingToEnum.Add(new EnumMapping(enumVal, value));
|
var enumVal = Enum.Parse<T>(member.Name);
|
||||||
if (!mappingToString.ContainsKey(enumVal))
|
#else
|
||||||
mappingToString.Add(enumVal, value);
|
var enumVal = (T)Enum.Parse(_enumType, member.Name);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
mappingStringToEnum.Add(new EnumMapping(enumVal, value));
|
||||||
|
if (!mappingEnumToString.ContainsKey(enumVal))
|
||||||
|
mappingEnumToString.Add(enumVal, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
_mappingToEnum = mappingToEnum.ToFrozenSet();
|
_mappingToEnum = mappingStringToEnum.ToFrozenSet();
|
||||||
_mappingToString = mappingToString.ToFrozenDictionary();
|
_mappingToString = mappingEnumToString.ToFrozenDictionary();
|
||||||
#else
|
#else
|
||||||
_mappingToEnum = mappingToEnum;
|
_mappingToEnum = mappingStringToEnum;
|
||||||
_mappingToString = mappingToString;
|
_mappingToString = mappingEnumToString;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For testing purposes only, allows resetting the static mapping and warnings
|
||||||
internal static void Reset()
|
internal static void Reset()
|
||||||
{
|
{
|
||||||
_undefinedEnumValue = null;
|
_undefinedEnumValue = null;
|
||||||
@ -336,7 +391,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static T? ParseString(string value)
|
public static T? ParseString(string value)
|
||||||
{
|
{
|
||||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
|
||||||
if (_mappingToEnum == null)
|
if (_mappingToEnum == null)
|
||||||
CreateMapping();
|
CreateMapping();
|
||||||
|
|
||||||
@ -369,8 +423,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
#if NET8_0_OR_GREATER
|
||||||
return (T)Enum.Parse(type, value, true);
|
return Enum.Parse<T>(value, true);
|
||||||
|
#else
|
||||||
|
return (T)Enum.Parse(_enumType, value, true);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -15,6 +15,9 @@ namespace CryptoExchange.Net.Testing
|
|||||||
|
|
||||||
if (message.Contains("Received null or empty enum value"))
|
if (message.Contains("Received null or empty enum value"))
|
||||||
throw new Exception("Enum null error: " + message);
|
throw new Exception("Enum null error: " + message);
|
||||||
|
|
||||||
|
if (message.Contains("Enum mapping sub-optimal."))
|
||||||
|
throw new Exception("Enum mapping error: " + message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void WriteLine(string? message)
|
public override void WriteLine(string? message)
|
||||||
@ -27,6 +30,9 @@ namespace CryptoExchange.Net.Testing
|
|||||||
|
|
||||||
if (message.Contains("Received null or empty enum value"))
|
if (message.Contains("Received null or empty enum value"))
|
||||||
throw new Exception("Enum null error: " + message);
|
throw new Exception("Enum null error: " + message);
|
||||||
|
|
||||||
|
if (message.Contains("Enum mapping sub-optimal."))
|
||||||
|
throw new Exception("Enum mapping error: " + message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user