mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2026-04-07 02:01:12 +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
|
||||
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
|
||||
private static FrozenDictionary<T, string>? _mappingToString = null;
|
||||
|
||||
private static bool RunOptimistic => true;
|
||||
#else
|
||||
private static List<EnumMapping>? _mappingToEnum = 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
|
||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||
|
||||
private static Type _enumType = typeof(T);
|
||||
private static T? _undefinedEnumValue;
|
||||
private static bool _hasFlagsAttribute = _enumType.IsDefined(typeof(FlagsAttribute));
|
||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||
|
||||
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)
|
||||
{
|
||||
isEmptyStringOrNull = false;
|
||||
var enumType = typeof(T);
|
||||
if (_mappingToEnum == null)
|
||||
CreateMapping();
|
||||
|
||||
if (RunOptimistic)
|
||||
{
|
||||
var resultOptimistic = GetValueOptimistic(ref reader);
|
||||
if (resultOptimistic != null)
|
||||
return resultOptimistic.Value;
|
||||
}
|
||||
|
||||
var stringValue = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
@ -173,8 +187,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
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))
|
||||
{
|
||||
isEmptyStringOrNull = true;
|
||||
@ -185,13 +200,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (!_unknownValuesWarned.Contains(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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -202,11 +226,32 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
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)
|
||||
{
|
||||
EnumMapping? mapping = null;
|
||||
// If we tried the optimistic path first we already know its not case match
|
||||
if (!RunOptimistic)
|
||||
{
|
||||
// Try match on full equals
|
||||
foreach (var item in _mappingToEnum)
|
||||
{
|
||||
@ -216,6 +261,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try matching ignoring case
|
||||
if (mapping == null)
|
||||
@ -237,10 +283,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
}
|
||||
|
||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
||||
if (_hasFlagsAttribute)
|
||||
{
|
||||
var intValue = int.Parse(value);
|
||||
result = (T)Enum.ToObject(objectType, intValue);
|
||||
result = (T)Enum.ToObject(_enumType, intValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -262,8 +308,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
result = (T)Enum.Parse(objectType, value, true);
|
||||
if (!Enum.IsDefined(objectType, result))
|
||||
#if NET8_0_OR_GREATER
|
||||
result = Enum.Parse<T>(value, true);
|
||||
#else
|
||||
result = (T)Enum.Parse(_enumType, value, true);
|
||||
#endif
|
||||
if (!Enum.IsDefined(_enumType, result))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
@ -280,11 +330,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
private static void CreateMapping()
|
||||
{
|
||||
var mappingToEnum = new List<EnumMapping>();
|
||||
var mappingToString = new Dictionary<T, string>();
|
||||
var mappingStringToEnum = new List<EnumMapping>();
|
||||
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)
|
||||
{
|
||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||
@ -292,23 +341,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
foreach (var value in attribute.Values)
|
||||
{
|
||||
var enumVal = (T)Enum.Parse(enumType, member.Name);
|
||||
mappingToEnum.Add(new EnumMapping(enumVal, value));
|
||||
if (!mappingToString.ContainsKey(enumVal))
|
||||
mappingToString.Add(enumVal, value);
|
||||
#if NET8_0_OR_GREATER
|
||||
var enumVal = Enum.Parse<T>(member.Name);
|
||||
#else
|
||||
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
|
||||
_mappingToEnum = mappingToEnum.ToFrozenSet();
|
||||
_mappingToString = mappingToString.ToFrozenDictionary();
|
||||
_mappingToEnum = mappingStringToEnum.ToFrozenSet();
|
||||
_mappingToString = mappingEnumToString.ToFrozenDictionary();
|
||||
#else
|
||||
_mappingToEnum = mappingToEnum;
|
||||
_mappingToString = mappingToString;
|
||||
_mappingToEnum = mappingStringToEnum;
|
||||
_mappingToString = mappingEnumToString;
|
||||
#endif
|
||||
}
|
||||
|
||||
// For testing purposes only, allows resetting the static mapping and warnings
|
||||
internal static void Reset()
|
||||
{
|
||||
_undefinedEnumValue = null;
|
||||
@ -336,7 +391,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <returns></returns>
|
||||
public static T? ParseString(string value)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
if (_mappingToEnum == null)
|
||||
CreateMapping();
|
||||
|
||||
@ -369,8 +423,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
return (T)Enum.Parse(type, value, true);
|
||||
#if NET8_0_OR_GREATER
|
||||
return Enum.Parse<T>(value, true);
|
||||
#else
|
||||
return (T)Enum.Parse(_enumType, value, true);
|
||||
#endif
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@ -15,6 +15,9 @@ namespace CryptoExchange.Net.Testing
|
||||
|
||||
if (message.Contains("Received null or empty enum value"))
|
||||
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)
|
||||
@ -27,6 +30,9 @@ namespace CryptoExchange.Net.Testing
|
||||
|
||||
if (message.Contains("Received null or empty enum value"))
|
||||
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