A problem with all of the approaches post so far is that they use GetCustomAttribute<EnumMemberAttribute>
for every lookup, which is somewhat expensive.
Given that attributes loaded from reflection are immutable it makes sense to cache the GetCustomAttribute<>
result in-memory for each enum type (TEnum
) being looked-up.
A static class<T>
that's generic over T
with a static initializer effectively acts as a singleton for every T
, which can be used to own an ImmutableDictionary
to efficiently and lazily cache the enum member attribute data:
Because the code below is generic over TEnum : struct, Enum
it means there is also no boxing of enum values and it supports enums of varying underlying-type (e.g. enum Foo : int
, enum Bar : long
, etc):
public static class EnumMemberNames
{
public static String? GetNameOrNull<TEnum>( TEnum value )
where TEnum : struct, Enum
{
return EnumAttribCache<TEnum>.cachedNames.TryGetValue( value, out String? text ) ? text : null;
}
public static String GetName<TEnum>( TEnum value )
where TEnum : struct, Enum
{
return GetNameOrNull( value ) ?? value.ToString();
}
private static class EnumAttribCache<TEnum>
where TEnum : struct, Enum
{
public static readonly ImmutableDictionary<TEnum,String> cachedNames = LoadNames();
private static ImmutableDictionary<TEnum,String> LoadNames()
{
return typeof(TEnum)
.GetTypeInfo()
.DeclaredFields
.Where( f => f.IsStatic && f.IsPublic && f.FieldType == typeof(TEnum) )
.Select( f => ( field: f, attrib: f.GetCustomAttribute<EnumMemberAttribute>() ) )
.Where( t => ( t.attrib?.IsValueSetExplicitly ?? false ) && !String.IsNullOrEmpty( t.attrib.Value ) )
.ToDictionary(
keySelector : t => (TEnum)t.field.GetValue( obj: null )!,
elementSelector: t => t.attrib!.Value!
)
.ToImmutableDictionary();
}
}
}
Used like so:
enum Foo
{
[EnumMemberAttribute( Value = "first" )]
A = 1,
[EnumMemberAttribute( Value = "second" )]
B = 2,
Unnamed = 4
}
public static void Main()
{
Console.WriteLine( EnumMemberNames.GetNameOrNull( Foo.A ) ); // "first"
Console.WriteLine( EnumMemberNames.GetNameOrNull( Foo.B ) ); // "second"
Console.WriteLine( EnumMemberNames.GetName( Foo.Unnamed ) ); // "Unnamed"
}
.SingleOrDefault(x => x.Name == value.ToString())
<-- Methinks that can be significantly optimized... – Gratulate