A simple and powerful C# code generator that lets you define string values for your enums to implement π₯π₯π₯ lookups. Greatly inspired by Andrew Lock's NetEscapades.EnumGenerators.
Did you ever need to get down to the nitty-gritty of trying to extract a DisplayAttribute
's Name
property for your enum values using reflection? It feels quite weird to reference System.ComponentModel
objects in modern code, doesn't it? No more! π With PodNet.EnumValues
you can simply create your enums as usual, and decorate the type with the provided [Values<T>]
, the enum members with [Value]
(or your own custom descendant of it), and you'll get magic extension methods that implement the lookup logic.
- π Simple.
- π Free.
- βοΈ Configurable.
- π₯ Fast.
- β¨ Awesome.
- Supports your C# 11+ codebase (generic attribute support required)
- .NET Standard 2.0, .NET Core 2.0+, .NET Framework 4.6.1+, .NET 5+ etc.
- Any IDE: Visual Studio, Rider or VS Code
- ...or no IDE, because it works from the command line using
dotnet
CLI
- ...or no IDE, because it works from the command line using
- This is an efficient incremental Roslyn source generator
- Supports trimming for small file sizes
- Highly configurable code generation
- Immediate feedback on your generated methods with helpful auto-generated IntelliSense comments
- Supports enums with β³
[Flags]
β³ - Great for simple (or complex) DIY localization scenarios, UI lookups for icons, colors, resources, cross-references, alternative keying, and lots more!
- Install the NuGet package to the project you want to use it in.
- Define your enums like you would normally.
public enum Sentiment { Happy, Neutral, Sad }
- Add
[Values<ValueAttribute>]
to the enum type and decorate the enum values with[Value]
:[Values<ValueAttribute>] public enum Sentiment { [Value("Green")] Happy, [Value("Yellow")] Neutral, [Value("Red")] Sad }
- As the magic happens in the background as you type, an extension method will get generated β by default, in the same namespace as the enum type, so you don't need to add any additional
using
s β, which you can simply call usingenum.GetValue()
:
![Screenshot from Visual Studio showing generated IntelliSense documentation and usage in action](./images/demo-extension-doc-comments.png)
There is interactive documentation generated for the methods as well. Parametrization of `[Values]` and changing the enum type's shape also changes the generated documentation accordingly (as well as the generated code, obviously). 1. You also get an analyzer and code fix if you miss a value:
![Screenshot from Visual Studio showing code fix in action](./images/demo-missing-member-code-fix.png)
You can also configure this warning (or any other generated warnings) to appear as an error, or silence them individually or globally (although the latter is not recommended).
Note
Unfortunately, it's not currently possible to offer a "rename" or "replace" functionality when generating code for string literal values, so you'll have to inspect and overwrite the generated name yourself (which defaults to the name of the enum member).
Important
Sometimes (mostly when doing file operations like renaming/moving files or directories, but rarely even when only editing the file contents), Visual Studio tends to not run the generator when typing, even though it should. The generator will run when executing Build or Rebuild in this case (or when it feels it's got enough time to in the background). This means you might not see the generated code, types, methods and reported diagnostics like warnings until you eventually build the project.
You can also try restarting Visual Studio or unloading and then reloading the project.
VS Code seems to have this issue more rarely than fat Visual Studio does.
Important
If you rename, move, delete or create some files/folders in Visual Studio, always check if VS did some unintended modifications to the *.csproj
file, like modifying some of your <AdditionalFiles>
ItemGroup
elements.
You can also inspect (but not modify) the generated code by navigating to {Project}/Dependencies/Analyzers/PodNet.EnumValues
in Visual Studio:
For reference and your viewing pleasure, we'll supply the code here as well (implementation details might change across package versions):
// <auto-generated /> #nullable enable using PodNet.EnumValues; namespace App.Users; public static partial class SentimentValueExtensions { /// <summary> /// Gets the associated <see cref="ValueAttribute"/> value for the <see cref="Sentiment"/> as follows: /// <code> /// [Unknown] = 0 => π₯ MissingEnumValueException π₯ /// [Happy] = 1 => "Green" /// [Neutral] = 2 => "Yellow" /// [Sad] = 3 => "Red" /// /// [(int)] => (int).ToString() /// </code> /// </summary> /// <remarks>Uses <see cref="UndefinedValueHandling.RawValueToString"/> for undefined values and <see cref="MissingValueHandling.ThrowMissingValueException"/> for missing values.</remarks> /// <param name="value">The <see cref="Sentiment"/> value.</param> /// <returns>A constant, deterministic string value representing the given <paramref name="value"/>.</returns> /// <exception cref="MissingEnumValueException">Thrown when the provided <paramref name="value"/> is declared as a named enum member but has no corresponding marker attribute of type <see cref="ValueAttribute"/> assigned to it.</exception> public static string GetValue(this Sentiment value) => value switch { Sentiment.Unknown => throw new MissingEnumValueException(typeof(Sentiment), value), Sentiment.Happy => "Green", Sentiment.Neutral => "Yellow", Sentiment.Sad => "Red", _ => ((int)value).ToString() }; } #nullable restore[!NOTE] The generated code now also accounts for the
Unknown
value throwing aMissingValueException
, and this is also reflected in the generated documentation. This is the case when you miss annotating a value with is correspondingValueAttribute
(or descendant, see below), and don't configure theValuesAttribute
further (see below as well).
You can see that the generator's basic functions are as easy as pie to use.
However, there are some advanced scenarios as well.
You can simply derive from ValueAttribute
similarly to:
public class ColorAttribute(string value) : ValueAttribute(value);
Important
It's a requirement for your custom descendant to have its first constructor argument be the string
value (but it can also be a referenced constant, not just inline). No other requirements, so you can supply more arguments if it suits your needs.
Note
If you're on C# 11.0, single declarations using primary constructors and bodyless types are unavailable, so you have to declare the attribute like so:
public class ColorAttribute : ValueAttribute
{
public ColorAttribute(string value) : base(value) { }
}
Note
C# is awesome.
Then, apply your own derived ValueAttribute
as the type parameter to [Values<T>]
:
[Values<ColorAttribute>] // π "ColorAttribute" instead of "ValueAttribute"
public enum Sentiment
{
[Color("Green")] Happy, // π \
[Color("Yellow")] Neutral, // π | [Color] instead of [Value]
[Color("Red")] Sad // π /
}
public static void PrintSentiment(Sentiment sentiment)
=> Console.WriteLine(sentiment.GetColor()); // π "GetColor()" instead of "GetValue()"
You can simply provide multiple attributes (and configure each separately) to generate multiple lookups for a single enum type.
public class SpanishAttribute(string value) : ValueAttribute(value);
public class FrenchAttribute(string value) : ValueAttribute(value);
// Supply both π "SpanishAttribute" and π "FrenchAttribute" to the type
[Values<SpanishAttribute>, Values<FrenchAttribute>]
/*
You could use multiple attribute lists as well, naturally:
[Values<SpanishAttribute>]
[Values<FrenchAttribute>]
Also note that you can configure all instances differently
*/
public enum Greeting
{
// π Supply both translations π to each value
[Spanish("Hola"), French("Salut")] Hi,
[Spanish("Buenos dΓas"), French("Bonjour")] GoodMorning,
[Spanish("Buenas noches"), French("Bonsoir")] GoodEvening
}
Console.WriteLine($"| English (raw name) | Spanish | French | {Environment.NewLine} | --- | --- | --- |");
foreach (var greeting in Greeting.GetValues<Greeting>())
{
Console.WriteLine($"| {greeting} | {greeting.GetSpanish()} | {greeting.GetFrench()}");
}
The above renders the following markdown table:
English (raw name) Spanish French Hi Hola Salut GoodMorning Buenos dΓas Bonjour GoodEvening Buenas noches Bonsoir
Using the previous method, you can simply customize the name of the extension method generated, and clarify the intent of the lookup by configuring its generated method name. However, you can also supply any other name for the method, and have other options as well. You have to configure βοΈ the [Values]
to do so.
The [Values<T>]
you can apply to enum types works out of the box as is with sensible defaults. You can override them if you wish as described in this section. The descriptions below are also available via the in-IDE IntelliSense.
[Values] Parameter |
Description |
---|---|
string? Namespace |
Namespace of the generated extension class. Default is to use the enum type's containing namespace. |
Accessibility Accessibility |
Accessibility/visibility of the generated extension class and method. Default is to inherit the enum type's accessibility. If you reuse the same class for the generated extensions, the accessibilities have to match. |
string? ClassName |
The extension class name. Default is "{EnumTypeName}ValueExtensions" . |
string? MethodName |
The name of the generated extension method. Leave null for the default, which is "Get{Attribute}" (translates to "GetValue" if TValue is ValueAttribute ). You can reuse the same partial class for different enum types (even with the same namespace, class, and method names), as they are overloadable if the enum types differ. |
UndefinedValueHandling UndefinedValueHandling |
Defines what should happen when an undefined enum value is encountered (often resulting from unsanitized/invalid user input or an invalid cast from a raw value). Not to be confused with MissingValueHandling , which handles values that are defined, but have no associated ValueAttribute . The default is UndefinedValueHandling.RawValueToString . |
MissingValueHandling MissingValueHandling |
Defines the behavior that is executed when a defined enum value has no associated ValueAttribute defined to it. The default is MissingValueHandling.ThrowMissingValueException , which also enables an analyzer that warns if an enum member hasn't defined the corresponding value with a TValue typed attribute. |
Accessibility
values
Describes the possible accessibility level of the type.
Values are the same as defined in the type
Microsoft.CodeAnalysis.Accessibility
(only the C# aliases). The only allowed values for an extension method areinternal
andpublic
, as they need to bestatic
, so defining inheritance behavior is not possible.
Accessibility |
---|
Internal = 4 |
Public = 6 |
UndefinedValueHandling
values
Defines what should happen when an undefined enum value is encountered (often resulting from unsanitized/invalid user input or an invalid cast from a raw value). Not to be confused with MissingValueHandling
, which handles values that are defined, but have no associated ValueAttribute
.
UndefinedValueHandling |
Description |
---|---|
RawValueToString |
The raw value itself is returned by calling object.ToString on the value. |
EmptyString |
Returns an empty string. This is useful if you want to implement a wrapper around the generated method and handle this case in user code. |
ThrowMissingValueException |
Throws a MissingEnumValueException . |
MissingValueHandling
values
Defines the behavior that is executed when a defined enum value has no associated ValueAttribute
defined to it.
MissingValueHandling |
Description |
---|---|
ThrowMissingValueException |
Throws a MissingEnumValueException if the enum value (or flag) has no value defined to it. This also enables an analyzer that warns if a defined enum value on the type has no corresponding annotation. |
ToString |
Returns the enum name as a string. Does not actually call object.ToString() , but rather returns the name as a constant string instance directly, which is faster. |
PascalCasing |
Returns the enum name as a PascalCasedString by swapping the first character to be uppercased. |
CamelCasing |
Returns the enum name as a camelCasedString . Assumes the name is in PascalCase. |
KebabCasing |
Returns the enum name as a kebab-cased-string . Assumes the name is in PascalCase. |
SnakeCasing |
Returns the enum name as a snake_cased_string . Assumes the name is in PascalCase. |
RawValueToString |
The raw constant value itself is returned by calling object.ToString() on the value. |
EmptyString |
Returns an empty string. This is useful if you want to implement a wrapper around the generated method and handle this case in user code. |
ToLowerInvariant |
Returns the enum name lowercased (culture invariant). |
ToUpperInvariant |
Returns the enum name uppercased (culture invariant). |
Additional arguments are provided to configure the [Flags]
behavior.
[Values] Parameter |
Description |
---|---|
IsFlags | Describes if the decorated enum type represents flag (binary) values. The generator defaults to true if [FlagsAttribute] is also present on the type declaration. When this evaluates to true, MissingValueHandling only takes into account values with a power of two and the 0 value, and UndefinedValueHandling pertains only to flag values that are larger than possible to represent using the given value (so, greater than or equal to twice the defined largest binary enum flag value).When using flags, a single value of the enum will be represented in binary, and all binary (power of two) values where a given value evaluates to having the given flag set will be concatenated by the generator. |
FlagsSeparator | Override the default separator when getting the flag values of the type if IsFlags evaluates to true (being set manually or by the enum type having a [FlagsAttribute] ); ignored otherwise. The default is `" |
You can also apply multiple [Values<T>]
instances to the same enum type, but the configurations have to be different (a sensible idea is to have different type parameters supplied).
Let's see an example.
namespace App.Comments;
[Values<ColorAttribute>] // π All defaults
[Values<EmojiAttribute>( // π All parameters are optional
Accessibility = Accessibility.Internal, // π Defaults to the enum's visibility, but can be public or internal
Namespace = "App.Helpers", // π Defaults to the enum's namespace (would be "App.Comments" here), overriding this implies you'll need to import the namespace yourself at the usage sites
ClassName = "EmojiExtensions", // π The default is "{EnumName}ValueExtensions"
MethodName = "GetEmojiForSentiment", // π The default is "Get{TValue}", in this case would be "GetEmoji"
MissingValueHandling = MissingValueHandling.EmptyString, // π Default is to throw when you forget to add [Emoji] to a member (also warns if a value is missing when this is set to throw)
UndefinedValueHandling = UndefinedValueHandling.ThrowMissingValueException // π Default is "RawValueToString", which would return the raw int value if it wasn't Happy (0), Neutral (1) or Sad (2)
)]
public enum Sentiment
{
[Emoji("π"), Color("Green")] Happy,
[Emoji("π"), Color("Yellow")] Neutral,
[Emoji("π"), Color("Red")] Sad
}
public static void PrintSentiment(Sentiment sentiment)
=> Console.WriteLine($"{sentiment.GetColor()} - {sentiment.GetEmojiForSentiment()}");
PrintSentiment(Sentiment.Happy); // "Green - π"
And finally, you can also use this with enums that are used as binary (bit) flags.
You'll get a warning if you miss a binary flag value (a power of two or the 0 value).
You can supply any non-flag value an override. You can do this for performance (if you have some common combinations), custom overrides, or special negative values (which throw an ArgumentOutOfRangeException
otherwise).
[Flags] // π Either add the [Flags]...
[Values<ValueAttribute>(
IsFlags = true, // ...or set IsFlags = true
FlagsSeparator = "" // The default is " | ", which represents a binary OR operation (the default in .NET's default Enum.ToString() as well).
)]
public enum Permissions
{
[Value("!")] Invalid = -1, // π You *can* supply any non-flag values, even negative values, as normal. Undefined negative values will throw an ArgumentOutOfRangeException, however.
[Value("-")] None = 0,
[Value("R")] Read = 1,
[Value("W")] Write = 2,
ReadWrite = Read | Write, // π You don't need to supply the value if you use shorthands...
[Value("X")] Execute = 4,
[Value("RX")] ReadExecute = Read | Execute, // π ...but you CAN, if you want. The provided value is only used if you don't supply your own separator.
WriteExecute = Write | Execute, // π You don't have to define all possible flag combinations, but it's shown here for completeness.
[Value("A")] All = Read | Write | Execute // π You can override the output with any custom values you like.
}
public static class PermissionPrinter
{
public static void PrintFlags(Permissions permissions)
{
Console.WriteLine(permissions.GetValue());
Console.WriteLine(permissions.GetValue("|")); // You can individually override the default separator (which would be " - ", but we've already overridden it in the [Values<>] configuration for Permissions
}
public static void PrintSome()
{
PrintFlags(Permissions.ReadWrite); // π Same as `(Permissions)3` or `Permissions.Read | Permissions.Write`
// π prints:
// RW // π Iterates through all binary flags: 1 (Read = R) and 2 (Write = W), and concatenates them using the configured empty ("") FlagsSeparator
// R|W // π GetValue("|") iterates and joins the values with the provided separator instead
PrintFlags(Permissions.Read | Permissions.Write | Permissions.Execute); // Same as "(Permissions)7" or "Permissions.All"
// π prints:
// A // π Get the ValueAttribute's value directly, if it is defined.
// R|W|X // π GetValue("|") iterates and joins the values with the provided separator, and ignores the [Value("A")] value.
PrintFlags((Permissions)15); // π This contains Read (1) | Write (2) | Execute (4) and an undefined 8
// π prints:
// RWX8 // π If you want to disallow this behavior, you can set UndefinedValueHandling to ThrowMissingValueException or EmptyString.
// R|W|X|8
}
}
Tip
Because the logic for enumerating the flag values is generated, you can go to the implementation (F12) and take a look yourself!
Code | Description | Notes | Fix |
---|---|---|---|
PN1601 | Value attribute is missing for enum value | Produced when you don't specify a MissingValueHandling other than the default ThrowMissingValueException AND an enum value has no corresponding [Value] assigned to it. If IsFlags is true, only takes into account possible single bit flag values (the None (0) value and positive powers of 2). |
Code fix: adds the indicated [Value] attribute to the enum value. Please note that it is currently not possible to set the caret's position to the string literal value that is auto-generated, nor is it possible to "refactor" that literal (literals cannot be refactored, as they are constant values). You'll have to manually edit the generated [Value("MyEnumName")] yourself after applying the fix.You can add the indicated attribute to the indicated value manually, remove the enum member, or change MissingValueHandling to any other value than the default ThrowMissingValueException , which will calculate the value based on the member's name. |
PN1602 | Enum types declared in generic types are not supported | Known limitation, see below. | Code fix: removes all [Values<>] attributes from the indicated enum type (but not the [Value] attributes from the members), thus preventing the generator to run at all.You can also move the enum type to a desired location outside the generic structure. |
PN1603 | Enum members with aliases can lead to unexpected results | The enum type declares aliases for some raw values. In enum MyEnum { V1, V2 = 0, V3 = V2 } , all 3 members are mapped to the raw 0 value. This leads to unexpected results if trying to look up the value 0 . Thus, this scenario is not supported. Also note that having shorthands for flags won't be a problem if they are not redundant. |
Remove the redundant declarations to that at most one is left for each underlying value. |
PN1604 | Undefined enum member for flag | If IsFlags is true (either by configuration or by having [Flags] on the enum type), and there are binary values missing that would need to be present to represent the largest declared enum member in the type, this warning is produced. |
Check if the largest declared enum member's value is declared correctly and correct as needed. If it is correct, check that all binary flag values needed to represent the largest declared value are correctly declared as well. The missing numeral values are indicated in the warning messages produced. |
This project aims to make it clean to define lookups for enum values. There are some known limitations, however, that are good to know about.
- No support for
System.ComponentModel.DisplayNameAttribute
,System.ComponentModel.DataAnnotations.DisplayAttribute
, or similar built-in or satellite attributes. This is intentional. The current implementation would blow up in size and complexity if we were to support multiple attribute types with varying semantics. However, if you think there should be some kind of additional support for this, we're all ears [Discussions].- There are other alternatives that target similar scenarios, including the awesome NetEscapades.EnumGenerators by the brilliant Andrew Lock. You should take a look at that package anyways!
- Enum types nested in generic class structures (for example,
MyNamespace.MyClass<T>.MyEnum
) are not supported (and produce the warningPN1602: Enum types declared in generic types are not supported
). It is possible to support this scenario, so let us know if you really want this implemented. Even better, send a pull request if you have it in you! :) The thing about this is that the generic type constraints have to be copied to the extension method declaration, which is quite cumbersome and finicky to implement. If you encounter this issue, it's recommended to move the enum type out of the generic structure, if possible. You can create a holder type with no type arguments, or just move it to the containing namespace or up the class hiererachy until no types are left. - If you reuse generated partial classes (override the
Namespace
and/orClassName
pairs to match in multiple declarations) for whatever reason, you have to match the providedAccessibility
values, as it is not possible to locate all partial declarations inside the generator. You'll get a compiler errorCS0262: Partial declarations of '{YourExtensionClass}' have conflicting accessibility modifiers
if this is the case. - The value can only be a
string
value at the moment. Integral values are inherently supported by the enum types themselves (although not multiple values), and I saw no other viable scenarios that suggest other values (asAttribute
s, these are constrained to a few constant types anyways) should be reasonable. Although it would be possible to supply another variant of the generator that generates the values using inversion of control (you could supply your custom class that takes the given enum type as a type parameter), it's not immediately obvious what purpose this would serve. If you want a similar feature, feel free to jump into the [Discussions].
Check if your PackageReference
is correct in the csproj
file. If you set the PackageReference
manually to exclude (or don't include) the compile
asset, the project you want to use the generator in won't see the assembly containing the attributes and enumerations you need to use the generator. It just won't fire up, because it's looking for the attribute on enum types.
The following are CORRECT references after installation:
<PackageReference Include="PodNet.EnumValues" Version="1.0.1"
PrivateAssets="all" />
<!-- This π adds the generator and diagnostic analyzers, including the compile-time reference as a private dependency. It means you can use the generator from the consuming project, but not from any project referencing your project. This is recommended for building NuGet packages others will consume. -->
<PackageReference Include="PodNet.EnumValues" Version="1.0.1" />
<!-- NOT setting it to PrivateAssets="all" will allow the consumers of your project to transitively access the functionality of the generator. This means that if you install the package in project A, then reference A from B, both of your projects will be able to use the generator and see the compile-time attributes. This is recommended for building apps. -->
Note
In version 1.0.0, the NuGet package was set up as a DevelopmentDependency
(as it should for analyzer packages), but this resulted in the compile-time assemblies not being correctly referenced. If you managed to somehow install 1.0.0 of the package, please upgrade to 1.0.1 to see the required attributes.
This project is intended to be widely usable, but no warranties are provided. If you want to contact us, feel free to do so in the org's [Discussions] or the project's topic, at our website at podnet.hu, or find us anywhere from LinkedIn to Meetup, YouTube or X.
Any kinds of contributions from issues to PRs and open discussions are welcome!
Don't forget to give us a β if you like this repo (it's free to give kudos!) or share it on socials!
If you're using our work or like what you see, consider supporting us. Every bit counts. π See here for more info.