Streamlining Enum Handling: Recent Updates to NetEscapades.EnumGenerators
Explore the latest enhancements in NetEscapades.EnumGenerators (v1.0.0-beta.16), a .NET source generator. This update introduces `[EnumMember]` support, improved metadata handling, new Roslyn analyzers to prevent common issues, and critical bug fixes, ensuring faster and more robust enum operations.
NetEscapades.EnumGenerators provides fast methods for working with enums in C#. This post details the recent updates to the NuGet package, covering its purpose, new features, analyzers, and bug fixes introduced in version 1.0.0-beta.16.
Why Use an Enum Source Generator?
NetEscapades.EnumGenerators was developed to address the surprisingly slow performance of certain enum operations, particularly Enum.ToString(). While .NET 8+ has brought improvements to runtime enum handling, historically, this has been a significant bottleneck.
Consider the following enum:
public enum Colour
{
Red = 0,
Blue = 1,
}
A common operation is to print the name of a Colour variable:
public void PrintColour(Colour colour)
{
Console.WriteLine("You chose " + colour.ToString());
// Example output: You chose Red
}
Despite its simple appearance, colour.ToString() can be inefficient. NetEscapades.EnumGenerators resolves this by automatically generating a faster ToStringFast() method. This generated method typically looks like this:
public static class ColourExtensions
{
public string ToStringFast(this Colour colour) =>
colour switch
{
Colour.Red => nameof(Colour.Red),
Colour.Blue => nameof(Colour.Blue),
_ => colour.ToString(),
};
}
This implementation uses a simple switch statement to directly map known enum values to their nameof string representation. For unknown values (e.g., (Colour)123), it safely falls back to the built-in ToString() method.
Benchmarking with BenchmarkDotNet demonstrates the performance difference:
| Method | FX | Mean | Error | StdDev | Ratio | Gen 0 | Allocated |
|---|---|---|---|---|---|---|---|
| ToString | net48 | 578.276 ns | 3.3109 ns | 3.0970 ns | 1.000 | 0.0458 | 96 B |
| ToStringFast | net48 | 3.091 ns | 0.0567 ns | 0.0443 ns | 0.005 | - | - |
| ToString | net6.0 | 17.985 ns | 0.1230 ns | 0.1151 ns | 1.000 | 0.0115 | 24 B |
| ToStringFast | net6.0 | 0.121 ns | 0.0225 ns | 0.0199 ns | 0.007 | - | - |
While these benchmarks are from older versions, the pattern remains consistent: ToStringFast() provides a significant performance boost over the built-in ToString(). This offers a "free" performance improvement, though actual results may vary depending on the specific enum.
For more details on the package's offerings, refer to its blog posts or the project's README.
Updates in 1.0.0-beta.16
Version 1.0.0-beta.16 of NetEscapades.EnumGenerators, released on November 4th, includes several quality-of-life features and bug fixes. These updates fall into three main categories:
- Redesign of "additional metadata attributes" like
[Display]and[Description]. - Introduction of additional analyzers to ensure
[EnumExtensions]is used correctly. - Bug fixes for edge cases.
Let's explore each update in detail.
Updated Metadata Attribute and [EnumMember] Support
Historically, [Display] or [Description] attributes could be applied to enum members to customize ToStringFast() or Parse behavior. For example:
[EnumExtensions]
public enum MyEnum
{
First,
[Display(Name = "2nd")]
Second,
}
This would generate multiple ToStringFast methods, including ToStringFastWithMetadata() which uses the custom Name from the [Display] attribute.
While [Display] and [Description] were long supported, a request was made to also support [EnumMember]. The previous implementation arbitrarily prioritized [Display] over [Description], which was not ideal. To address this and the addition of [EnumMember], the generator now supports only a single type of metadata attribute for a given enum.
This selection is made via the MetadataSource property on the [EnumExtensions] attribute. For instance, to explicitly use [Display] attributes:
[EnumExtensions(MetadataSource = MetadataSource.DisplayAttribute)]
public enum EnumWithDisplayNameInNamespace
{
First = 0,
[Display(Name = "2nd")]
Second = 1,
Third = 2,
}
Any other metadata attributes ([Description], [EnumMember]) applied to members in this enum would be ignored. Setting MetadataSource.None will prevent the emission of overloads that rely on metadata attributes.
Important Breaking Change: The default metadata source has been switched to [EnumMember], which is considered a more semantically appropriate choice. If you're currently using other metadata attributes, this change will impact you. You can override the project-wide default by setting the EnumGenerator_EnumMetadataSource property in your project file:
<PropertyGroup>
<EnumGenerator_EnumMetadataSource>DisplayAttribute</EnumGenerator_EnumMetadataSource>
</PropertyGroup>
An analyzer may be added in a future release to warn about this potential issue.
New Analyzers to Warn of Incorrect Usage
The package now includes Roslyn analyzers to identify and warn about scenarios where generated code might not compile due to edge cases, which can be confusing for developers.
Flagging Generated Extension Class Name Clashes (NEEG001)
It's possible to decorate enums with [EnumExtensions] attributes such that they attempt to generate extension classes with identical names, leading to compilation errors. For example:
namespace SomeNamespace;
[EnumExtensions]
public enum MyEnum
{
One,
Two
}
public class Nested
{
[EnumExtensions]
public enum MyEnum
{
One,
Two
}
}
In this case, both enums would try to generate SomeNamespace.MyEnumExtensions. While disambiguation (e.g., nesting the extension class) would be ideal, extension method classes cannot be nested. Other disambiguation strategies can also lead to clashes, especially since the class name can be explicitly set.
To address this, analyzer NEEG001 now flags such name clashes directly on the [EnumExtensions] attribute as an error diagnostic. This makes the cause of the compilation errors more obvious.
Handling Enums Nested in Generic Types (NEEG002)
The generator cannot produce valid code when an enum is nested inside a generic type, such as:
using NetEscapades.EnumGenerators;
public class Nested<T> // Type is generic
{
[EnumExtensions]
public enum MyEnum // Enum is nested inside
{
First,
Second,
}
}
Extension method classes cannot be placed inside nested types like Nested<T>, and making the extension class itself generic introduces significant complexity. Therefore, this scenario is not supported. Analyzer NEEG002 is applied to the [EnumExtensions] attribute in such cases, warning that the code is invalid and no extension methods will be generated.
Duplicate Case Labels in an Enum (NEEG003)
This analyzer addresses enums with "duplicate" members—i.e., members that share the same underlying value, like Failed = 2 and Error = 2:
[EnumExtensions]
public enum Status
{
Unknown = 0,
Pending = 1,
Failed = 2,
Error = 2, // NEEG003: Enum has duplicate values and will give inconsistent values for ToStringFast()
}
While perfectly valid C#, this can lead to unexpected behavior with ToStringFast() (and the built-in ToString()), where calling ToString() on Status.Error might yield "Failed." This is an artifact of how enums work in .NET. Analyzer NEEG003 flags these cases as an Info diagnostic. It won't break the build, but it highlights a potential for unexpected results, urging developers to be aware of how generated extensions might behave.
Bug Fixes
Several bugs have also been resolved in this release.
C#14 Extension Members with LangVersion=Preview
Previous releases added support for C#14 Extension Members, allowing static extension methods to be called as if they were defined directly on the enum type:
[EnumExtensions]
public enum MyColours
{
Red, Green, Blue,
}
// Previously: MyColoursExtensions.Parse("Red")
var colour = MyColours.Parse("Red"); // With C#14 Extension Members
Initially, this feature was inadvertently enabled when LangVersion=Preview was set, which could lead to inconsistent behavior depending on the SDK version. The fix (#165 and #172) now restricts generation of extension members to C#14 or higher, excluding the Preview case. To re-enable extension members when using Preview, you can explicitly set EnumGenerator_ForceExtensionMembers to true in your project file. This setting was accidentally defaulted to true in #165 but corrected to false by default in #172.
Handling Reserved Words in Enum Member Names
The generator previously failed to correctly handle enum member names that are C# reserved words, such as string in the following example:
[EnumExtensions]
public enum AttributeFieldType
{
number,
@string, // reserved, so escaped with @
date
}
This resulted in invalid generated code because the reserved word was not properly escaped, causing compilation errors:
public static string ToStringFast(this AttributeFieldType value) =>
value switch
{
global::AttributeFieldType.number => "number",
global::AttributeFieldType.string => "string", // ❌ Does not compile
global::AttributeFieldType.date => "date",
_ => value.AsUnderlyingType().ToString(),
};
The fix involves using SyntaxFacts.GetKeywordKind to detect reserved words and prepend @ for correct escaping, ensuring the generated code compiles successfully:
public static string ToStringFast(this AttributeFieldType value) =>
value switch
{
global::AttributeFieldType.number => "number",
global::AttributeFieldType.@string => "string", // ✅ Correctly escaped
global::AttributeFieldType.date => "date",
_ => value.AsUnderlyingType().ToString(),
};
Removal of NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES Option
The NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES option, which allowed embedding marker attributes directly into the target DLL, has been removed (#160). This option was rarely the correct approach, as the package already ships attributes in a dedicated DLL. Removing it reduces duplication, simplifies testing configurations, and potentially allows for future helper types within the attributes DLL.
Summary
This post detailed the recent updates to NetEscapades.EnumGenerators in version 1.0.0-beta.16. These quality-of-life enhancements include refined [EnumMember] and metadata attribute support, new analyzers to catch common pitfalls, and critical bug fixes for edge cases. It is recommended to update and try out the new features. If any problems are encountered, logging an issue on GitHub is encouraged.