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

View File

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