< Summary

Information
Line coverage
25%
Covered lines: 108
Uncovered lines: 321
Coverable lines: 429
Total lines: 722
Line coverage: 25.1%
Branch coverage
19%
Covered branches: 46
Total branches: 231
Branch coverage: 19.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

C:\h\w\B31A098C\w\BB5A0A33\e\runtime-utils\Runner\runtime\src\libraries\System.Text.Json\src\System\Text\Json\Serialization\Converters\Value\EnumConverter.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Collections.Concurrent;
 6using System.Collections.Generic;
 7using System.Diagnostics;
 8using System.Reflection;
 9using System.Runtime.CompilerServices;
 10using System.Text.Encodings.Web;
 11using System.Text.Json.Nodes;
 12using System.Text.Json.Schema;
 13
 14namespace System.Text.Json.Serialization.Converters
 15{
 16    internal sealed class EnumConverter<T> : JsonPrimitiveConverter<T> // Do not rename FQN (legacy schema generation)
 17        where T : struct, Enum
 18    {
 219        private static readonly TypeCode s_enumTypeCode = Type.GetTypeCode(typeof(T));
 20
 21        // Odd type codes are conveniently signed types (for enum backing types).
 222        private static readonly bool s_isSignedEnum = ((int)s_enumTypeCode % 2) == 1;
 223        private static readonly bool s_isFlagsEnum = typeof(T).IsDefined(typeof(FlagsAttribute), inherit: false);
 24
 25        private readonly EnumConverterOptions _converterOptions; // Do not rename (legacy schema generation)
 26        private readonly JsonNamingPolicy? _namingPolicy; // Do not rename (legacy schema generation)
 27
 28        /// <summary>
 29        /// Stores metadata for the individual fields declared on the enum.
 30        /// </summary>
 31        private readonly EnumFieldInfo[] _enumFieldInfo;
 32
 33        /// <summary>
 34        /// Defines a case-insensitive index of enum field names to their metadata.
 35        /// In case of casing conflicts, extra fields are appended to a list in the value.
 36        /// This is the main dictionary that is queried by the enum parser implementation.
 37        /// </summary>
 38        private readonly Dictionary<string, EnumFieldInfo> _enumFieldInfoIndex;
 39
 40        /// <summary>
 41        /// Holds a cache from enum value to formatted UTF-8 text including flag combinations.
 42        /// <see cref="ulong"/> is as the key used rather than <typeparamref name="T"/> given measurements that
 43        /// show private memory savings when a single type is used https://github.com/dotnet/runtime/pull/36726#discussi
 44        /// </summary>
 45        private readonly ConcurrentDictionary<ulong, JsonEncodedText> _nameCacheForWriting;
 46
 47        /// <summary>
 48        /// Holds a mapping from input text to enum values including flag combinations and alternative casings.
 49        /// </summary>
 50        private readonly ConcurrentDictionary<string, ulong> _nameCacheForReading;
 51
 52        // This is used to prevent flooding the cache due to exponential bitwise combinations of flags.
 53        // Since multiple threads can add to the cache, a few more values might be added.
 54        private const int NameCacheSizeSoftLimit = 64;
 55
 53856        public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOption
 53857        {
 53858            Debug.Assert(EnumConverterFactory.Helpers.IsSupportedTypeCode(s_enumTypeCode));
 59
 53860            _converterOptions = converterOptions;
 53861            _namingPolicy = namingPolicy;
 53862            _enumFieldInfo = ResolveEnumFields(namingPolicy);
 53863            _enumFieldInfoIndex = new(StringComparer.OrdinalIgnoreCase);
 64
 53865            _nameCacheForWriting = new();
 53866            _nameCacheForReading = new(StringComparer.Ordinal);
 67
 53868            JavaScriptEncoder? encoder = options.Encoder;
 161469            foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 070            {
 071                AddToEnumFieldIndex(fieldInfo);
 72
 073                JsonEncodedText encodedName = JsonEncodedText.Encode(fieldInfo.JsonName, encoder);
 074                _nameCacheForWriting.TryAdd(fieldInfo.Key, encodedName);
 075                _nameCacheForReading.TryAdd(fieldInfo.JsonName, fieldInfo.Key);
 076            }
 77
 53878            if (namingPolicy != null)
 079            {
 80                // Additionally populate the field index with the default names of fields that used a naming policy.
 81                // This is done to preserve backward compat: default names should still be recognized by the parser.
 082                foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 083                {
 084                    if (fieldInfo.Kind is EnumFieldNameKind.NamingPolicy)
 085                    {
 086                        AddToEnumFieldIndex(new EnumFieldInfo(fieldInfo.Key, EnumFieldNameKind.Default, fieldInfo.Origin
 087                    }
 088                }
 089            }
 90
 91            void AddToEnumFieldIndex(EnumFieldInfo fieldInfo)
 092            {
 093                if (!_enumFieldInfoIndex.TryAdd(fieldInfo.JsonName, fieldInfo))
 094                {
 95                    // We have a casing conflict, append field to the existing entry.
 096                    EnumFieldInfo existingFieldInfo = _enumFieldInfoIndex[fieldInfo.JsonName];
 097                    existingFieldInfo.AppendConflictingField(fieldInfo);
 098                }
 099            }
 538100        }
 101
 102        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 1430103        {
 1430104            switch (reader.TokenType)
 105            {
 630106                case JsonTokenType.String when (_converterOptions & EnumConverterOptions.AllowStrings) != 0:
 342107                    if (TryParseEnumFromString(ref reader, out T result))
 26108                    {
 26109                        return result;
 110                    }
 198111                    break;
 112
 296113                case JsonTokenType.Number when (_converterOptions & EnumConverterOptions.AllowNumbers) != 0:
 296114                    switch (s_enumTypeCode)
 115                    {
 460116                        case TypeCode.Int32 when reader.TryGetInt32(out int int32): return (T)(object)int32;
 0117                        case TypeCode.UInt32 when reader.TryGetUInt32(out uint uint32): return (T)(object)uint32;
 0118                        case TypeCode.Int64 when reader.TryGetInt64(out long int64): return (T)(object)int64;
 0119                        case TypeCode.UInt64 when reader.TryGetUInt64(out ulong uint64): return (T)(object)uint64;
 0120                        case TypeCode.Byte when reader.TryGetByte(out byte ubyte8): return (T)(object)ubyte8;
 0121                        case TypeCode.SByte when reader.TryGetSByte(out sbyte byte8): return (T)(object)byte8;
 0122                        case TypeCode.Int16 when reader.TryGetInt16(out short int16): return (T)(object)int16;
 0123                        case TypeCode.UInt16 when reader.TryGetUInt16(out ushort uint16): return (T)(object)uint16;
 124                    }
 132125                    break;
 126            }
 127
 1122128            ThrowHelper.ThrowJsonException();
 129            return default;
 190130        }
 131
 132        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
 0133        {
 0134            EnumConverterOptions converterOptions = _converterOptions;
 0135            if ((converterOptions & EnumConverterOptions.AllowStrings) != 0)
 0136            {
 0137                ulong key = ConvertToUInt64(value);
 138
 0139                if (_nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted))
 0140                {
 0141                    writer.WriteStringValue(formatted);
 0142                    return;
 143                }
 144
 0145                if (IsDefinedValueOrCombinationOfValues(key))
 0146                {
 0147                    Debug.Assert(s_isFlagsEnum, "Should only be entered by flags enums.");
 0148                    string stringValue = FormatEnumAsString(key, value, dictionaryKeyPolicy: null);
 0149                    if (_nameCacheForWriting.Count < NameCacheSizeSoftLimit)
 0150                    {
 0151                        formatted = JsonEncodedText.Encode(stringValue, options.Encoder);
 0152                        writer.WriteStringValue(formatted);
 0153                        _nameCacheForWriting.TryAdd(key, formatted);
 0154                    }
 155                    else
 0156                    {
 157                        // We also do not create a JsonEncodedText instance here because passing the string
 158                        // directly to the writer is cheaper than creating one and not caching it for reuse.
 0159                        writer.WriteStringValue(stringValue);
 0160                    }
 161
 0162                    return;
 163                }
 0164            }
 165
 0166            if ((converterOptions & EnumConverterOptions.AllowNumbers) == 0)
 0167            {
 0168                ThrowHelper.ThrowJsonException();
 169            }
 170
 0171            if (s_isSignedEnum)
 0172            {
 0173                writer.WriteNumberValue(ConvertToInt64(value));
 0174            }
 175            else
 0176            {
 0177                writer.WriteNumberValue(ConvertToUInt64(value));
 0178            }
 0179        }
 180
 181        internal override T ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions 
 0182        {
 183            // NB JsonSerializerOptions.DictionaryKeyPolicy is ignored on deserialization.
 184            // This is true for all converters that implement dictionary key serialization.
 185
 0186            if (!TryParseEnumFromString(ref reader, out T result))
 0187            {
 0188                ThrowHelper.ThrowJsonException();
 189            }
 190
 0191            return result;
 0192        }
 193
 194        internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, JsonSerializerOptions options, bo
 0195        {
 0196            JsonNamingPolicy? dictionaryKeyPolicy = options.DictionaryKeyPolicy is { } dkp && dkp != _namingPolicy ? dkp
 0197            ulong key = ConvertToUInt64(value);
 198
 0199            if (dictionaryKeyPolicy is null && _nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted))
 0200            {
 0201                writer.WritePropertyName(formatted);
 0202                return;
 203            }
 204
 0205            if (IsDefinedValueOrCombinationOfValues(key))
 0206            {
 0207                Debug.Assert(s_isFlagsEnum || dictionaryKeyPolicy != null, "Should only be entered by flags enums or dic
 0208                string stringValue = FormatEnumAsString(key, value, dictionaryKeyPolicy);
 0209                if (dictionaryKeyPolicy is null && _nameCacheForWriting.Count < NameCacheSizeSoftLimit)
 0210                {
 211                    // Only attempt to cache if there is no dictionary key policy.
 0212                    formatted = JsonEncodedText.Encode(stringValue, options.Encoder);
 0213                    writer.WritePropertyName(formatted);
 0214                    _nameCacheForWriting.TryAdd(key, formatted);
 0215                }
 216                else
 0217                {
 218                    // We also do not create a JsonEncodedText instance here because passing the string
 219                    // directly to the writer is cheaper than creating one and not caching it for reuse.
 0220                    writer.WritePropertyName(stringValue);
 0221                }
 222
 0223                return;
 224            }
 225
 0226            if (s_isSignedEnum)
 0227            {
 0228                writer.WritePropertyName(ConvertToInt64(value));
 0229            }
 230            else
 0231            {
 0232                writer.WritePropertyName(key);
 0233            }
 0234        }
 235
 236        private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T result)
 342237        {
 342238            Debug.Assert(reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName);
 239
 342240            int bufferLength = reader.ValueLength;
 342241            char[]? rentedBuffer = null;
 242            bool success;
 243
 342244            Span<char> charBuffer = bufferLength <= JsonConstants.StackallocCharThreshold
 342245                ? stackalloc char[JsonConstants.StackallocCharThreshold]
 342246                : (rentedBuffer = ArrayPool<char>.Shared.Rent(bufferLength));
 247
 342248            int charsWritten = reader.CopyString(charBuffer);
 224249            charBuffer = charBuffer.Slice(0, charsWritten);
 250#if NET
 224251            ReadOnlySpan<char> source = charBuffer.Trim();
 224252            ConcurrentDictionary<string, ulong>.AlternateLookup<ReadOnlySpan<char>> lookup = _nameCacheForReading.GetAlt
 253#else
 254            string source = ((ReadOnlySpan<char>)charBuffer).Trim().ToString();
 255            ConcurrentDictionary<string, ulong> lookup = _nameCacheForReading;
 256#endif
 224257            if (lookup.TryGetValue(source, out ulong key))
 15258            {
 15259                result = ConvertFromUInt64(key);
 15260                success = true;
 15261                goto End;
 262            }
 263
 209264            if (JsonHelpers.IntegerRegex.IsMatch(source))
 11265            {
 266                // We found an integer that is not an enum field name.
 11267                if ((_converterOptions & EnumConverterOptions.AllowNumbers) != 0)
 11268                {
 11269                    success = Enum.TryParse(source, out result);
 11270                }
 271                else
 0272                {
 0273                    result = default;
 0274                    success = false;
 0275                }
 11276            }
 277            else
 198278            {
 198279                success = TryParseNamedEnum(source, out result);
 198280            }
 281
 209282            if (success && _nameCacheForReading.Count < NameCacheSizeSoftLimit)
 11283            {
 11284                lookup.TryAdd(source, ConvertToUInt64(result));
 11285            }
 286
 224287        End:
 224288            if (rentedBuffer != null)
 0289            {
 0290                charBuffer.Clear();
 0291                ArrayPool<char>.Shared.Return(rentedBuffer);
 0292            }
 293
 224294            return success;
 224295        }
 296
 297        private bool TryParseNamedEnum(
 298#if NET
 299            ReadOnlySpan<char> source,
 300#else
 301            string source,
 302#endif
 303            out T result)
 198304        {
 305#if NET
 198306            Dictionary<string, EnumFieldInfo>.AlternateLookup<ReadOnlySpan<char>> lookup = _enumFieldInfoIndex.GetAltern
 198307            ReadOnlySpan<char> rest = source;
 308#else
 309            Dictionary<string, EnumFieldInfo> lookup = _enumFieldInfoIndex;
 310            ReadOnlySpan<char> rest = source.AsSpan();
 311#endif
 198312            ulong key = 0;
 313
 314            do
 198315            {
 316                ReadOnlySpan<char> next;
 198317                int i = rest.IndexOf(',');
 198318                if (i == -1)
 198319                {
 198320                    next = rest;
 198321                    rest = default;
 198322                }
 323                else
 0324                {
 0325                    next = rest.Slice(0, i).TrimEnd();
 0326                    rest = rest.Slice(i + 1).TrimStart();
 0327                }
 328
 198329                if (lookup.TryGetValue(
 198330#if NET
 198331                        next,
 198332#else
 198333                        next.ToString(),
 198334#endif
 198335                        out EnumFieldInfo? firstResult) &&
 198336                    firstResult.GetMatchingField(next) is EnumFieldInfo match)
 0337                {
 0338                    key |= match.Key;
 0339                    continue;
 340                }
 341
 198342                result = default;
 198343                return false;
 344
 0345            } while (!rest.IsEmpty);
 346
 0347            result = ConvertFromUInt64(key);
 0348            return true;
 198349        }
 350
 351        private static ulong ConvertToUInt64(T value)
 11352        {
 11353            return s_enumTypeCode switch
 11354            {
 11355                TypeCode.Int32 => (ulong)(int)(object)value,
 0356                TypeCode.UInt32 => (uint)(object)value,
 0357                TypeCode.Int64 => (ulong)(long)(object)value,
 0358                TypeCode.UInt64 => (ulong)(object)value,
 0359                TypeCode.Int16 => (ulong)(short)(object)value,
 0360                TypeCode.UInt16 => (ushort)(object)value,
 0361                TypeCode.SByte => (ulong)(sbyte)(object)value,
 0362                _ => (byte)(object)value
 11363            };
 11364        }
 365
 366        private static long ConvertToInt64(T value)
 0367        {
 0368            Debug.Assert(s_isSignedEnum);
 0369            return s_enumTypeCode switch
 0370            {
 0371                TypeCode.Int32 => (int)(object)value,
 0372                TypeCode.Int64 => (long)(object)value,
 0373                TypeCode.Int16 => (short)(object)value,
 0374                _ => (sbyte)(object)value,
 0375            };
 0376        }
 377
 378        private static T ConvertFromUInt64(ulong value)
 15379        {
 15380            return s_enumTypeCode switch
 15381            {
 15382                TypeCode.Int32 => (T)(object)(int)value,
 0383                TypeCode.UInt32 => (T)(object)(uint)value,
 0384                TypeCode.Int64 => (T)(object)(long)value,
 0385                TypeCode.UInt64 => (T)(object)value,
 0386                TypeCode.Int16 => (T)(object)(short)value,
 0387                TypeCode.UInt16 => (T)(object)(ushort)value,
 0388                TypeCode.SByte => (T)(object)(sbyte)value,
 0389                _ => (T)(object)(byte)value
 15390            };
 15391        }
 392
 393        /// <summary>
 394        /// Attempt to format the enum value as a comma-separated string of flag values, or returns false if not a valid
 395        /// </summary>
 396        private string FormatEnumAsString(ulong key, T value, JsonNamingPolicy? dictionaryKeyPolicy)
 0397        {
 0398            Debug.Assert(IsDefinedValueOrCombinationOfValues(key), "must only be invoked against valid enum values.");
 0399            Debug.Assert(
 0400                s_isFlagsEnum || (dictionaryKeyPolicy is not null && Enum.IsDefined(typeof(T), value)),
 0401                "must either be a flag type or computing a dictionary key policy.");
 402
 0403            if (s_isFlagsEnum)
 0404            {
 0405                using ValueStringBuilder sb = new(stackalloc char[JsonConstants.StackallocCharThreshold]);
 0406                ulong remainingBits = key;
 407
 0408                foreach (EnumFieldInfo enumField in _enumFieldInfo)
 0409                {
 0410                    ulong fieldKey = enumField.Key;
 0411                    if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey)
 0412                    {
 0413                        remainingBits &= ~fieldKey;
 0414                        string name = dictionaryKeyPolicy is not null
 0415                            ? ResolveAndValidateJsonName(enumField.OriginalName, dictionaryKeyPolicy, enumField.Kind)
 0416                            : enumField.JsonName;
 417
 0418                        if (sb.Length > 0)
 0419                        {
 0420                            sb.Append(", ");
 0421                        }
 422
 0423                        sb.Append(name);
 424
 0425                        if (remainingBits == 0)
 0426                        {
 0427                            break;
 428                        }
 0429                    }
 0430                }
 431
 0432                Debug.Assert(remainingBits == 0 && sb.Length > 0, "unexpected remaining bits or empty string.");
 0433                return sb.ToString();
 434            }
 435            else
 0436            {
 0437                Debug.Assert(dictionaryKeyPolicy != null);
 438
 0439                foreach (EnumFieldInfo enumField in _enumFieldInfo)
 0440                {
 441                    // Search for an exact match and apply the key policy.
 0442                    if (enumField.Key == key)
 0443                    {
 0444                        return ResolveAndValidateJsonName(enumField.OriginalName, dictionaryKeyPolicy, enumField.Kind);
 445                    }
 0446                }
 447
 0448                Debug.Fail("should not have been reached.");
 449                return null;
 450            }
 0451        }
 452
 453        private bool IsDefinedValueOrCombinationOfValues(ulong key)
 0454        {
 0455            if (s_isFlagsEnum)
 0456            {
 0457                ulong remainingBits = key;
 458
 0459                foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 0460                {
 0461                    ulong fieldKey = fieldInfo.Key;
 0462                    if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey)
 0463                    {
 0464                        remainingBits &= ~fieldKey;
 465
 0466                        if (remainingBits == 0)
 0467                        {
 0468                            return true;
 469                        }
 0470                    }
 0471                }
 472
 0473                return false;
 474            }
 475            else
 0476            {
 0477                foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 0478                {
 0479                    if (fieldInfo.Key == key)
 0480                    {
 0481                        return true;
 482                    }
 0483                }
 484
 0485                return false;
 486            }
 0487        }
 488
 489        internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling)
 0490        {
 0491            if ((_converterOptions & EnumConverterOptions.AllowStrings) != 0)
 0492            {
 493                // This explicitly ignores the integer component in converters configured as AllowNumbers | AllowStrings
 494                // which is the default for JsonStringEnumConverter. This sacrifices some precision in the schema for si
 495
 0496                if (s_isFlagsEnum)
 0497                {
 498                    // Do not report enum values in case of flags.
 0499                    return new() { Type = JsonSchemaType.String };
 500                }
 501
 0502                JsonArray enumValues = [];
 0503                foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 0504                {
 0505                    enumValues.Add((JsonNode)fieldInfo.JsonName);
 0506                }
 507
 0508                return new() { Enum = enumValues };
 509            }
 510
 0511            return new() { Type = JsonSchemaType.Integer };
 0512        }
 513
 514        private static EnumFieldInfo[] ResolveEnumFields(JsonNamingPolicy? namingPolicy)
 538515        {
 516#if NET
 538517            string[] names = Enum.GetNames<T>();
 538518            T[] values = Enum.GetValues<T>();
 519#else
 520            string[] names = Enum.GetNames(typeof(T));
 521            T[] values = (T[])Enum.GetValues(typeof(T));
 522#endif
 538523            Debug.Assert(names.Length == values.Length);
 524
 538525            Dictionary<string, string>? enumMemberAttributes = null;
 1614526            foreach (FieldInfo field in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static))
 0527            {
 0528                if (field.GetCustomAttribute<JsonStringEnumMemberNameAttribute>() is { } attribute)
 0529                {
 0530                    (enumMemberAttributes ??= new(StringComparer.Ordinal)).Add(field.Name, attribute.Name);
 0531                }
 0532            }
 533
 538534            var enumFields = new EnumFieldInfo[names.Length];
 1076535            for (int i = 0; i < names.Length; i++)
 0536            {
 0537                string originalName = names[i];
 0538                T value = values[i];
 0539                ulong key = ConvertToUInt64(value);
 540                EnumFieldNameKind kind;
 541
 0542                if (enumMemberAttributes != null && enumMemberAttributes.TryGetValue(originalName, out string? attribute
 0543                {
 0544                    originalName = attributeName;
 0545                    kind = EnumFieldNameKind.Attribute;
 0546                }
 547                else
 0548                {
 0549                    kind = namingPolicy != null ? EnumFieldNameKind.NamingPolicy : EnumFieldNameKind.Default;
 0550                }
 551
 0552                string jsonName = ResolveAndValidateJsonName(originalName, namingPolicy, kind);
 0553                enumFields[i] = new EnumFieldInfo(key, kind, originalName, jsonName);
 0554            }
 555
 538556            if (s_isFlagsEnum)
 0557            {
 558                // Perform topological sort for flags enums to ensure values that are supersets of other values come fir
 559                // This is important for flags enums to ensure proper parsing and formatting.
 0560                enumFields = TopologicalSortEnumFields(enumFields);
 0561            }
 562
 538563            return enumFields;
 538564        }
 565
 566        private static string ResolveAndValidateJsonName(string name, JsonNamingPolicy? namingPolicy, EnumFieldNameKind 
 0567        {
 0568            if (kind is not EnumFieldNameKind.Attribute && namingPolicy is not null)
 0569            {
 570                // Do not apply a naming policy to names that are explicitly set via attributes.
 571                // This is consistent with JsonPropertyNameAttribute semantics.
 0572                name = namingPolicy.ConvertName(name);
 0573            }
 574
 0575            if (string.IsNullOrEmpty(name) || char.IsWhiteSpace(name[0]) || char.IsWhiteSpace(name[name.Length - 1]) ||
 0576                (s_isFlagsEnum && name.Contains(',')))
 0577            {
 578                // Reject null or empty strings or strings with leading or trailing whitespace.
 579                // In the case of flags additionally reject strings containing commas.
 0580                ThrowHelper.ThrowInvalidOperationException_UnsupportedEnumIdentifier(typeof(T), name);
 581            }
 582
 0583            return name;
 0584        }
 585
 0586        private sealed class EnumFieldInfo(ulong key, EnumFieldNameKind kind, string originalName, string jsonName)
 587        {
 588            private List<EnumFieldInfo>? _conflictingFields;
 0589            public EnumFieldNameKind Kind { get; } = kind;
 0590            public ulong Key { get; } = key;
 0591            public string OriginalName { get; } = originalName;
 0592            public string JsonName { get; } = jsonName;
 593
 594            /// <summary>
 595            /// Assuming we have field that conflicts with the current up to case sensitivity,
 596            /// append it to a list of trailing values for use by the enum value parser.
 597            /// </summary>
 598            public void AppendConflictingField(EnumFieldInfo other)
 0599            {
 0600                Debug.Assert(JsonName.Equals(other.JsonName, StringComparison.OrdinalIgnoreCase), "The conflicting entry
 601
 0602                if (ConflictsWith(this, other))
 0603                {
 604                    // Silently discard if the preceding entry is the default or has identical name.
 0605                    return;
 606                }
 607
 0608                List<EnumFieldInfo> conflictingFields = _conflictingFields ??= [];
 609
 610                // Walk the existing list to ensure we do not add duplicates.
 0611                foreach (EnumFieldInfo conflictingField in conflictingFields)
 0612                {
 0613                    if (ConflictsWith(conflictingField, other))
 0614                    {
 0615                        return;
 616                    }
 0617                }
 618
 0619                conflictingFields.Add(other);
 620
 621                // Determines whether the first field info matches everything that the second field info matches,
 622                // in which case the second field info is redundant and doesn't need to be added to the list.
 623                static bool ConflictsWith(EnumFieldInfo current, EnumFieldInfo other)
 0624                {
 625                    // The default name matches everything case-insensitively.
 0626                    if (current.Kind is EnumFieldNameKind.Default)
 0627                    {
 0628                        return true;
 629                    }
 630
 631                    // current matches case-sensitively since it's not the default name.
 632                    // other matches case-insensitively, so it matches more than current.
 0633                    if (other.Kind is EnumFieldNameKind.Default)
 0634                    {
 0635                        return false;
 636                    }
 637
 638                    // Both are case-sensitive so they need to be identical.
 0639                    return current.JsonName.Equals(other.JsonName, StringComparison.Ordinal);
 0640                }
 0641            }
 642
 643            public EnumFieldInfo? GetMatchingField(ReadOnlySpan<char> input)
 0644            {
 0645                Debug.Assert(input.Equals(JsonName.AsSpan(), StringComparison.OrdinalIgnoreCase), "Must equal the field 
 646
 0647                if (Kind is EnumFieldNameKind.Default || input.SequenceEqual(JsonName.AsSpan()))
 0648                {
 649                    // Default enum names use case insensitive parsing so are always a match.
 0650                    return this;
 651                }
 652
 0653                if (_conflictingFields is { } conflictingFields)
 0654                {
 0655                    Debug.Assert(conflictingFields.Count > 0);
 0656                    foreach (EnumFieldInfo matchingField in conflictingFields)
 0657                    {
 0658                        if (matchingField.Kind is EnumFieldNameKind.Default || input.SequenceEqual(matchingField.JsonNam
 0659                        {
 0660                            return matchingField;
 661                        }
 0662                    }
 0663                }
 664
 0665                return null;
 0666            }
 667        }
 668
 669        /// <summary>
 670        /// Performs a topological sort on enum fields to ensure values that are supersets of other values come first.
 671        /// </summary>
 672        private static EnumFieldInfo[] TopologicalSortEnumFields(EnumFieldInfo[] enumFields)
 0673        {
 0674            if (enumFields.Length <= 1)
 0675            {
 0676                return enumFields;
 677            }
 678
 0679            var indices = new (int negativePopCount, int index)[enumFields.Length];
 0680            for (int i = 0; i < enumFields.Length; i++)
 0681            {
 682                // We want values with more bits set to come first so negate the pop count.
 683                // Keep the index as a second comparand so that sorting stability is preserved.
 0684                indices[i] = (-PopCount(enumFields[i].Key), i);
 0685            }
 686
 0687            Array.Sort(indices);
 688
 0689            var sortedFields = new EnumFieldInfo[enumFields.Length];
 0690            for (int i = 0; i < indices.Length; i++)
 0691            {
 692                // extract the index from the sorted tuple
 0693                int index = indices[i].index;
 0694                sortedFields[i] = enumFields[index];
 0695            }
 696
 0697            return sortedFields;
 0698        }
 699
 700        private static int PopCount(ulong value)
 0701        {
 702#if NET
 0703            return (int)ulong.PopCount(value);
 704#else
 705            int count = 0;
 706            while (value != 0)
 707            {
 708                value &= value - 1;
 709                count++;
 710            }
 711            return count;
 712#endif
 0713        }
 714
 715        private enum EnumFieldNameKind
 716        {
 717            Default = 0,
 718            NamingPolicy = 1,
 719            Attribute = 2,
 720        }
 721    }
 722}

Methods/Properties

.cctor()
.ctor(System.Text.Json.Serialization.Converters.EnumConverterOptions,System.Text.Json.JsonNamingPolicy,System.Text.Json.JsonSerializerOptions)
AddToEnumFieldIndex(System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>)
Read(System.Text.Json.Utf8JsonReader&,System.Type,System.Text.Json.JsonSerializerOptions)
Write(System.Text.Json.Utf8JsonWriter,T,System.Text.Json.JsonSerializerOptions)
ReadAsPropertyNameCore(System.Text.Json.Utf8JsonReader&,System.Type,System.Text.Json.JsonSerializerOptions)
WriteAsPropertyNameCore(System.Text.Json.Utf8JsonWriter,T,System.Text.Json.JsonSerializerOptions,System.Boolean)
TryParseEnumFromString(System.Text.Json.Utf8JsonReader&,T&)
TryParseNamedEnum(System.ReadOnlySpan`1<System.Char>,T&)
ConvertToUInt64(T)
ConvertToInt64(T)
ConvertFromUInt64(System.UInt64)
FormatEnumAsString(System.UInt64,T,System.Text.Json.JsonNamingPolicy)
IsDefinedValueOrCombinationOfValues(System.UInt64)
GetSchema(System.Text.Json.Serialization.JsonNumberHandling)
ResolveEnumFields(System.Text.Json.JsonNamingPolicy)
ResolveAndValidateJsonName(System.String,System.Text.Json.JsonNamingPolicy,System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldNameKind<T>)
.ctor(System.UInt64,System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldNameKind<T>,System.String,System.String)
Kind()
Key()
OriginalName()
JsonName()
AppendConflictingField(System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>)
ConflictsWith(System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>,System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>)
GetMatchingField(System.ReadOnlySpan`1<System.Char>)
TopologicalSortEnumFields(System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>[])
PopCount(System.UInt64)