using CryptoExchange.Net.Attributes; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; namespace CryptoExchange.Net.Converters.SystemTextJson { /// /// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value /// public class EnumConverter : JsonConverterFactory { private bool _warnOnMissingEntry = true; private bool _writeAsInt; private static readonly ConcurrentDictionary>> _mapping = new(); /// /// public EnumConverter() { } /// /// /// /// public EnumConverter(bool writeAsInt, bool warnOnMissingEntry) { _writeAsInt = writeAsInt; _warnOnMissingEntry = warnOnMissingEntry; } /// public override bool CanConvert(Type typeToConvert) { return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true; } /// 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> AddMapping(Type objectType) { var mapping = new List>(); 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(Enum.Parse(objectType, member.Name), value)); } } _mapping.TryAdd(objectType, mapping); return mapping; } private class EnumConverterInner : JsonConverter { private bool _warnOnMissingEntry = true; private bool _writeAsInt; public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry) { _warnOnMissingEntry = warnOnMissingEntry; _writeAsInt = writeAsInt; } public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert; if (!_mapping.TryGetValue(enumType, out var mapping)) mapping = AddMapping(enumType); var stringValue = reader.TokenType switch { JsonTokenType.String => reader.GetString(), JsonTokenType.Number => reader.GetInt16().ToString(), JsonTokenType.True => reader.GetBoolean().ToString(), JsonTokenType.False => reader.GetBoolean().ToString(), JsonTokenType.Null => null, _ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType) }; if (string.IsNullOrEmpty(stringValue)) { // 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, mapping, stringValue!, out var result)) { var defaultValue = GetDefaultValue(typeToConvert, enumType); if (string.IsNullOrWhiteSpace(stringValue)) { if (defaultValue != null) // 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 { // We received an enum value but weren't able to parse it. 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"); } return (T?)defaultValue; } return (T?)result; } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { if (value == null) { writer.WriteNullValue(); } else { if (!_writeAsInt) { var stringValue = GetString(value.GetType(), value); writer.WriteStringValue(stringValue); } else { writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int))); } } } private static object? GetDefaultValue(Type objectType, Type enumType) { if (Nullable.GetUnderlyingType(objectType) != null) return null; return Activator.CreateInstance(enumType); // return default value } private static bool GetValue(Type objectType, List> enumMapping, string value, out object? result) { // 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)); if (mapping.Equals(default(KeyValuePair))) mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); if (!mapping.Equals(default(KeyValuePair))) { result = mapping.Key; return true; } try { // If no explicit mapping is found try to parse string result = Enum.Parse(objectType, value, true); return true; } catch (Exception) { result = default; return false; } } } /// /// 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 /// /// /// /// [return: NotNullIfNotNull("enumValue")] public static string? GetString(T enumValue) => GetString(typeof(T), enumValue); /// /// 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 /// /// /// /// [return: NotNullIfNotNull("enumValue")] public static string? GetString(Type objectType, object? enumValue) { objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; if (!_mapping.TryGetValue(objectType, out var mapping)) mapping = AddMapping(objectType); return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); } /// /// Get the enum value from a string /// /// Enum type /// String value /// public static T? ParseString(string value) where T : Enum { var type = typeof(T); if (!_mapping.TryGetValue(type, out var enumMapping)) enumMapping = AddMapping(type); var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); if (mapping.Equals(default(KeyValuePair))) mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); if (!mapping.Equals(default(KeyValuePair))) { return (T)mapping.Key; } try { // If no explicit mapping is found try to parse string return (T)Enum.Parse(type, value, true); } catch (Exception) { return default; } } } }