| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using System.ComponentModel; |
| | | 3 | | |
| | | 4 | | namespace Allyaria.Abstractions.Extensions; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Provides extension methods for working with <see cref="Enum" /> values, including retrieving user-friendly descripti |
| | | 8 | | /// from <see cref="DescriptionAttribute" /> with sensible fallbacks and caching for performance. |
| | | 9 | | /// </summary> |
| | | 10 | | /// <remarks> |
| | | 11 | | /// Descriptions are cached per (enum <see cref="Type" />, raw enum <c>name</c>) to avoid repeated reflection. For |
| | | 12 | | /// <c>[Flags]</c> combinations (e.g., <c>"Read, Write"</c>), each constituent name is resolved independently and then |
| | | 13 | | /// re-joined using <c>", "</c>. |
| | | 14 | | /// </remarks> |
| | | 15 | | public static class EnumExtensions |
| | | 16 | | { |
| | | 17 | | /// <summary> |
| | | 18 | | /// Cache of resolved descriptions keyed by a tuple of the enum <see cref="Type" /> and the raw member <c>name</c>. |
| | | 19 | | /// avoids repeated reflection and attribute lookups across calls. |
| | | 20 | | /// </summary> |
| | 3 | 21 | | private static readonly ConcurrentDictionary<(Type type, string name), string> DescriptionCache = new(); |
| | | 22 | | |
| | | 23 | | /// <summary>Retrieves a user-friendly description for an <see cref="Enum" /> value.</summary> |
| | | 24 | | /// <param name="value">The enumeration value whose description to retrieve.</param> |
| | | 25 | | /// <returns> |
| | | 26 | | /// The <see cref="DescriptionAttribute.Description" /> value when present. Otherwise, a humanized fallback derived |
| | | 27 | | /// the enum member name(s). For <c>[Flags]</c> combinations, each constituent name is individually humanized and th |
| | | 28 | | /// results are joined with <c>", "</c>. |
| | | 29 | | /// </returns> |
| | | 30 | | /// <remarks> |
| | | 31 | | /// Fallback humanization is performed via <see cref="StringExtensions.FromPascalCase(string?)" />. The result is ca |
| | | 32 | | /// per (enum type, raw name) pair. |
| | | 33 | | /// </remarks> |
| | | 34 | | /// <exception cref="AryArgumentException">Thrown if <paramref name="value" /> is <see langword="null" />.</exceptio |
| | | 35 | | public static string GetDescription(this Enum value) |
| | | 36 | | { |
| | 1476 | 37 | | AryGuard.NotNull(value: value); |
| | | 38 | | |
| | 1474 | 39 | | var type = value.GetType(); |
| | 1474 | 40 | | var name = value.ToString(); |
| | | 41 | | |
| | 1474 | 42 | | return DescriptionCache.GetOrAdd( |
| | 1474 | 43 | | key: (type, name), |
| | 1474 | 44 | | valueFactory: static key => |
| | 1474 | 45 | | { |
| | 239 | 46 | | (var t, var n) = key; |
| | 1474 | 47 | | |
| | 239 | 48 | | if (!n.Contains(value: ',')) |
| | 1474 | 49 | | { |
| | 237 | 50 | | return GetSingleDescription(enumType: t, memberName: n); |
| | 1474 | 51 | | } |
| | 1474 | 52 | | |
| | 2 | 53 | | var parts = n.Split(separator: ',') |
| | 4 | 54 | | .Select(selector: s => s.Trim()) |
| | 4 | 55 | | .Where(predicate: s => s.Length > 0) |
| | 6 | 56 | | .Select(selector: part => GetSingleDescription(enumType: t, memberName: part)); |
| | 1474 | 57 | | |
| | 2 | 58 | | return string.Join(separator: ", ", values: parts); |
| | 1474 | 59 | | } |
| | 1474 | 60 | | ); |
| | | 61 | | } |
| | | 62 | | |
| | | 63 | | /// <summary>Strongly-typed convenience overload that defers to <see cref="GetDescription(Enum)" />.</summary> |
| | | 64 | | /// <typeparam name="TEnum">The enum type.</typeparam> |
| | | 65 | | /// <param name="value">The enumeration value whose description to retrieve.</param> |
| | | 66 | | /// <returns>The description string as defined by <see cref="GetDescription(Enum)" />.</returns> |
| | | 67 | | public static string GetDescription<TEnum>(this TEnum value) |
| | | 68 | | where TEnum : struct, Enum |
| | 1454 | 69 | | => ((Enum)value).GetDescription(); |
| | | 70 | | |
| | | 71 | | /// <summary>Resolves the description for a single enum member name on a given enum <see cref="Type" />.</summary> |
| | | 72 | | /// <param name="enumType">The enum <see cref="Type" /> that defines the member.</param> |
| | | 73 | | /// <param name="memberName">The exact member name to resolve (not a composite <c>[Flags]</c> string).</param> |
| | | 74 | | /// <returns> |
| | | 75 | | /// The <see cref="DescriptionAttribute.Description" /> value if present and non-whitespace; otherwise the |
| | | 76 | | /// <paramref name="memberName" /> humanized via <see cref="StringExtensions.FromPascalCase(string?)" />. |
| | | 77 | | /// </returns> |
| | | 78 | | private static string GetSingleDescription(Type enumType, string memberName) |
| | | 79 | | { |
| | 241 | 80 | | var memberInfos = enumType.GetMember(name: memberName); |
| | | 81 | | |
| | 241 | 82 | | if (memberInfos.Length <= 0) |
| | | 83 | | { |
| | 2 | 84 | | return memberName.FromPascalCase(); |
| | | 85 | | } |
| | | 86 | | |
| | 239 | 87 | | var attr = memberInfos[0] |
| | 239 | 88 | | .GetCustomAttributes(attributeType: typeof(DescriptionAttribute), inherit: false) |
| | 239 | 89 | | .OfType<DescriptionAttribute>() |
| | 239 | 90 | | .FirstOrDefault(); |
| | | 91 | | |
| | 239 | 92 | | if (attr is not null && !string.IsNullOrWhiteSpace(value: attr.Description)) |
| | | 93 | | { |
| | 217 | 94 | | return attr.Description; |
| | | 95 | | } |
| | | 96 | | |
| | 22 | 97 | | return memberName.FromPascalCase(); |
| | | 98 | | } |
| | | 99 | | } |