1
0
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:
JKorf 2026-04-05 20:04:52 +02:00
parent 8f2adaabe2
commit 406ae08c17
2 changed files with 91 additions and 28 deletions

View File

@ -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,18 +226,40 @@ 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;
// Try match on full equals
foreach (var item in _mappingToEnum)
// If we tried the optimistic path first we already know its not case match
if (!RunOptimistic)
{
if (item.StringValue.Equals(value, StringComparison.Ordinal))
// Try match on full equals
foreach (var item in _mappingToEnum)
{
mapping = item;
break;
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
}
}
@ -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)
{

View File

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