π§ WORK IN PROGRESS π§
Enum-generator is a zero-dependency library containing a source generator that generates useful extension methods and utilities for enums in C#.
- Fast string conversions β
ToStringFast()compiles to aswitchover the enum members instead of the reflection-basedEnum.ToString(), andFromStringFast(string)does the reverse. - UTF-8 support β
AsUtf8Span()returns aReadOnlySpan<byte>backed by au8literal, so no allocation or encoding is needed.NullTerminatedMemberNamesexposes all member names as a single null-terminated UTF-8 blob (useful for native interop, such as Dear ImGui combo boxes). - Allocation-free
ValuesβValuesis a cachedIReadOnlyList<TEnum>, unlikeEnum.GetValues<TEnum>()which allocates a new array on every call. - Fast validation β
IsDefined()checks aHashSetof the underlying values instead of using reflection. - Flags support β flags enums get
HasFlagFast()(no boxing, unlikeEnum.HasFlag) andContainsDefinedFlagsOnly(), and theirToStringFast()composes and caches combined names like"A, B". - Index mapping β
GetIndex()andFromIndex(int)map members to their declaration order, which is handy for arrays and UI lists. - Binary serialization β
BinaryWriter.Write(TEnum)andBinaryReader.Read{EnumName}()round-trip the enum using its underlying type, so abyteenum takes one byte. - Custom display names β
[Display(Name = "...")]on a member overrides the name used by all string conversions. - Enums you don't own β assembly-level
[GenerateEnumUtilities<T>]generates utilities for enums from the BCL or third-party libraries. - All underlying types β
byte,sbyte,short,ushort,int,uint,long, andulongare supported. - Zero dependencies β the package contains nothing but the source generator. The attributes are generated into your own project, so there is no assembly to reference and nothing is added to your runtime dependencies.
dotnet add package NoahStolk.EnumGeneratorThis is an analyzer-only package, so the PackageReference it writes needs no further editing:
<PackageReference Include="NoahStolk.EnumGenerator">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>The generated code uses UTF-8 string literals and Enum.GetValues<T>(), so consuming projects need C# 11 or newer and .NET 5 or newer. The analyzer itself requires .NET SDK 9.0.3xx (or VS 2022 17.14) or newer.
The [GenerateEnumUtilities] attributes are generated into your compilation as internal types in the EnumGenerator namespace β there is no assembly to reference, and the package leaves no trace in your build output or in the dependencies of any package you publish.
Mark an enum with [GenerateEnumUtilities]:
using EnumGenerator;
namespace MyApp;
[GenerateEnumUtilities]
internal enum Language
{
CSharp,
CPlusPlus,
}This generates a static LanguageGen class in the same namespace, with the same accessibility as the enum:
Language.CSharp.ToStringFast(); // "CSharp"
Language.CSharp.AsUtf8Span(); // "CSharp"u8
Language.CSharp.GetIndex(); // 0
Language.CSharp.IsDefined(); // true
((Language)7).IsDefined(); // false
LanguageGen.Values; // [Language.CSharp, Language.CPlusPlus]
LanguageGen.FromStringFast("C++"); // Language.CPlusPlus
LanguageGen.FromIndex(1); // Language.CPlusPlus
LanguageGen.NullTerminatedMemberNames; // "CSharp\0CPlusPlus\0"u8Methods that receive a value outside the enum's defined members throw an ArgumentOutOfRangeException.
[Display(Name = "...")] overrides the name used by ToStringFast, FromStringFast, AsUtf8Span, and NullTerminatedMemberNames:
using System.ComponentModel.DataAnnotations;
[GenerateEnumUtilities]
internal enum Language
{
[Display(Name = "C#")]
CSharp,
[Display(Name = "C++")]
CPlusPlus,
}Language.CSharp.ToStringFast(); // "C#"
LanguageGen.FromStringFast("C++"); // Language.CPlusPlusEnums marked with [Flags] get a different set of utilities β HasFlagFast and ContainsDefinedFlagsOnly instead of FromStringFast and IsDefined:
[Flags]
[GenerateEnumUtilities]
internal enum FlagsType
{
None = 0,
A = 1,
B = 2,
C = 4,
D = 8,
E = 16,
}const FlagsType ab = FlagsType.A | FlagsType.B;
ab.HasFlagFast(FlagsType.A); // true
ab.HasFlagFast(FlagsType.C); // false
ab.ToStringFast(); // "A, B"
ab.AsUtf8Span(); // "A, B"u8
ab.ContainsDefinedFlagsOnly(); // true
((FlagsType)32).ContainsDefinedFlagsOnly(); // falseComposed names are built once and cached, so repeated ToStringFast calls on the same combination don't reallocate. Members that are not a power of two (aliases such as All = A | B | C) are skipped, and 0 is only included when a member is declared for it.
Use the generic attribute at the assembly level to generate utilities for enums from other assemblies, such as the BCL or a NuGet package:
using EnumGenerator;
using Silk.NET.OpenGL;
[assembly: GenerateEnumUtilities<DayOfWeek>]
[assembly: GenerateEnumUtilities<BlendEquationModeEXT>]DayOfWeek.Sunday.ToStringFast(); // "Sunday"
BlendEquationModeEXT.FuncAdd.GetIndex(); // 0
DayOfWeekGen.Values; // all seven daysThe generated class is placed in the enum's own namespace (System for DayOfWeek) and is always public. Note that [Display] attributes are not read for external enums, since the generator only sees their metadata.
Enums with duplicate values (common in generated interop bindings, where several names map to the same constant) are deduplicated β the first declared member wins.
The default class name is {EnumName}Gen. Both attributes accept an override, either positionally or as a named argument:
[GenerateEnumUtilities(GeneratedClassName = "LanguageUtils")]
internal enum Language { /* ... */ }
[assembly: GenerateEnumUtilities<DayOfWeek>(GeneratedClassName = "DayOfWeekUtils")]For the Language enum from the first example, the generator emits Language.g.cs:
// <auto-generated>
// This code was generated by EnumGenerator.
// </auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
namespace MyApp;
internal static class LanguageGen
{
private static readonly HashSet<int> _definedValues = new()
{
(int)MyApp.Language.CSharp,
(int)MyApp.Language.CPlusPlus,
};
public static IReadOnlyList<MyApp.Language> Values { get; } = Enum.GetValues<MyApp.Language>();
public static ReadOnlySpan<byte> NullTerminatedMemberNames => "CSharp\0CPlusPlus\0"u8;
public static string ToStringFast(this MyApp.Language value)
{
return value switch
{
MyApp.Language.CSharp => "CSharp",
MyApp.Language.CPlusPlus => "CPlusPlus",
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
}
// AsUtf8Span, FromStringFast, GetIndex, FromIndex, Write, ReadLanguage, IsDefined
}TODO
To debug the source generator, use the launchSettings.json file in the EnumGenerator project to run the generator against the EnumGenerator.Sample project.
You can also debug the generator tests using the EnumGenerator.Tests project.
To simply accept all snapshots immediately, run
./scripts/accept-all.sh src/EnumGenerator.Tests/snapshotsfrom the root of the repository.
To control which diff tool is used for snapshot testing, use the DiffEngine_ToolOrder environment variable.
In JetBrains Rider, this can be configured under Build, Execution, Deployment > Unit Testing > Test Runner > Environment variables.
You can also disable DiffEngine by setting the DiffEngine_Disable environment variable to true.