{"slug": "recent-updates-to-netescapades-enumgenerators-enummember-support-analyzers-and", "title": "Recent updates to NetEscapades.EnumGenerators: [EnumMember] support, analyzers, and bug fixes", "summary": "The article summarizes recent updates to the NetEscapades.EnumGenerators NuGet package, a source generator that creates fast methods for working with enums in .NET. The updates in version 1.0.0-beta.16 include redesigned support for metadata attributes like `[Display]` and `[Description]`, new analyzers to ensure correct usage of the `[EnumExtensions]` attribute, and bug fixes for edge cases. The package generates optimized `ToStringFast()` methods using switch statements, which are significantly faster than the built-in `ToString()` for known enum values.", "body_md": "In this post I describe some of the recent updates to my source generator NuGet package [NetEscapades.EnumGenerators](https://github.com/andrewlock/NetEscapades.EnumGenerators) which you can use to add fast methods for working with `enum`\n\ns. I start by describing why the package exists and what you can use it for, then I walk through some of the recent changes.\n\n[Why should you use an enum source generator?](#why-should-you-use-an-enum-source-generator-)\n\n[NetEscapades.EnumGenerators](https://github.com/andrewlock/NetEscapades.EnumGenerators) was one of the first source generators I created using [the incremental generator support introduced in .NET 6](/exploring-dotnet-6-part-9-source-generator-updates-incremental-generators/). I chose to create this package to work around an annoying characteristic of working with enums: some operations are surprisingly slow.\n\nNote that while this has\n\nhistoricallybeen true, this fact won't necessarily remain true forever. In fact, .NET 8+ provided a bunch of improvements to enum handling in the runtime.\n\nAs an example, let's say you have the following enum:\n\n```\npublic enum Colour\n{\n    Red = 0,\n    Blue = 1,\n}\n```\n\nAt some point, you want to print the name of a `Color`\n\nvariable, so you create this helper method:\n\n```\npublic void PrintColour(Colour colour)\n{\n    Console.WriteLine(\"You chose \"+ colour.ToString()); // You chose Red\n}\n```\n\nWhile this *looks* like it should be fast, it's really not. *NetEscapades.EnumGenerators* works by automatically generating an implementation that *is* fast. It generates a `ToStringFast()`\n\nmethod that looks something like this:\n\n```\npublic static class ColourExtensions\n{\n    public string ToStringFast(this Colour colour)\n        => colour switch\n        {\n            Colour.Red => nameof(Colour.Red),\n            Colour.Blue => nameof(Colour.Blue),\n            _ => colour.ToString(),\n        }\n    }\n}\n```\n\nThis simple switch statement checks for each of the known values of `Colour`\n\nand uses `nameof`\n\nto return the textual representation of the `enum`\n\n. If it's an unknown value, then it falls back to the built-in `ToString()`\n\nimplementation to ensure correct handling of unknown values (for example this is valid C#: `PrintColour((Colour)123)`\n\n).\n\nIf we compare these two implementations using [BenchmarkDotNet](https://benchmarkdotnet.org/) for a known colour, you can see how much faster `ToStringFast()`\n\nimplementation is:\n\n| Method | FX | Mean | Error | StdDev | Ratio | Gen 0 | Allocated |\n|---|---|---|---|---|---|---|---|\n| ToString | `net48` | 578.276 ns | 3.3109 ns | 3.0970 ns | 1.000 | 0.0458 | 96 B |\n| ToStringFast | `net48` | 3.091 ns | 0.0567 ns | 0.0443 ns | 0.005 | - | - |\n| ToString | `net6.0` | 17.985 ns | 0.1230 ns | 0.1151 ns | 1.000 | 0.0115 | 24 B |\n| ToStringFast | `net6.0` | 0.121 ns | 0.0225 ns | 0.0199 ns | 0.007 | - | - |\n\nThese numbers are obviously quite old now, but the overall pattern hasn't changed: .NET is *way* faster than .NET Framework, and the `ToStringFast()`\n\nimplementation is way faster than the built-in `ToString()`\n\n. Obviously your mileage may vary and the results will depend on the specific enum you're using, but in general, using the source generator should give you a free performance boost.\n\nIf you want to learn more about what the package provides, check my\n\n[blog posts]or see the project[README].\n\nThat covers the basics, now let's look at what's new.\n\n[Updates in 1.0.0-beta.16](#updates-in-1-0-0-beta-16)\n\nVersion [1.0.0-beta16 of NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators/) was released to nuget.org on 4th November and included a number of quality of life features and bug fixes. I'll describe each of the updates in more detail below, but they fall into one of three categories:\n\n- Redesign of how \"additional metadata attributes\" such as\n`[Display]`\n\nand`[Description]`\n\nwork. - Additional analyzers to ensure\n`[EnumExtensions]`\n\nis used correctly - Bug fixes for edge cases\n\nLet's start by looking at the updated metadata attribute support.\n\n[Updated metadata attribute and ](#updated-metadata-attribute-and-enummember-support)`[EnumMember]`\n\nsupport\n\n`[EnumMember]`\n\nsupportFor a long time, you've been able to use `[Display]`\n\nor `[Description]`\n\nattributes applied to `enum`\n\nmembers to customize how `ToStringFast`\n\nor `Parse`\n\nworks with the library. For example, if you have the following `enum`\n\n:\n\n```\n[EnumExtensions]\npublic enum MyEnum\n{\n    First,\n\n    [Display(Name = \"2nd\")]\n    Second,\n}\n```\n\nThen three different `ToString`\n\nmethods are generated: Two overloads of `ToStringFast()`\n\nand `ToStringFastWithMetadata()`\n\n:\n\n```\npublic static partial class MyEnumExtensions\n{\n    // Use a boolean to decide whether to use \"metadata\" attributes\n    public static string ToStringFast(this MyEnum value, bool useMetadataAttributes)\n        => useMetadataAttributes ? value.ToStringFastWithMetadata() : value.ToStringFast();\n\n    // Use the raw enum member names\n    public static string ToStringFast(this MyEnum value)\n        => value switch\n        {\n            MyEnum.First => nameof(MyEnum.First),\n            MyEnum.Second => nameof(MyEnum.Second),\n            _ => value.ToString(),\n        };\n\n    // Use metadata attributes if provided, and fallback to raw enum member names\n    private static string ToStringFastWithMetadata(this MyEnum value)\n        => value switch\n        {\n            MyEnum.First => nameof(MyEnum.First),\n            MyEnum.Second => \"2nd\", // 👈 from the metadata names\n            _ => value.ToString(),\n        };\n    // ... more generated members\n}\n```\n\nThe ability to use these additional metadata values can be very useful, and I've used them frequently. For a long time I supported `[Display]`\n\nand `[Description]`\n\nattributes, but [there was a request](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/73) to support `[EnumMember]`\n\nas well.\n\nThe problem was when you had *multiple* metadata attributes on enum members—which one should the attribute use? Previously the generator arbitrarily chose `[Display]`\n\npreferentially, and fell back to `[Description]`\n\n. But there was no good reason for that ordering, it was entirely due to one being implemented before the other😬 And adding `[EnumMember]`\n\nas *another* fallback just felt too nasty.😅\n\nSo instead, [in #163](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/163), I added explicit support for `[EnumMember]`\n\nbut also updated the code so that you could only use a *single* metadata attribute source for a given enum. That means only a single *type* of metadata attribute is considered for a given enum.\n\nYou can select the source to use by setting the `MetadataSource`\n\nproperty on the `[EnumExtensions]`\n\nattribute. In the example below, the generated source explicitly opts in to using `[Display]`\n\nattributes:\n\n```\n[EnumExtensions(MetadataSource = MetadataSource.DisplayAttribute)]\npublic enum EnumWithDisplayNameInNamespace\n{\n    First = 0,\n    [Display(Name = \"2nd\")]\n    Second = 1,\n    Third = 2,\n}\n```\n\nAny other metadata attributes (`[Description]`\n\n, `[EnumMember]`\n\n) applied to members in the above `enum`\n\nwould be ignored.\n\nAlternatively, you can use `MetadataSource.None`\n\nto choose *none* of the metadata attributes. In this case, the overloads that take a `useMetadataAttributes`\n\nparameter will not be emitted.\n\nThis was a breaking change on its own, but there was an even bigger change: the\n\ndefaultmetadata source has been changed to`[EnumMember]`\n\nas a better semantic choice for these attributes.\n\nYou can change the default metadata source to use for a whole project by setting the `EnumGenerator_EnumMetadataSource`\n\nproperty in your project:\n\n```\n<PropertyGroup>\n  <EnumGenerator_EnumMetadataSource>DisplayAttribute</EnumGenerator_EnumMetadataSource>\n</PropertyGroup>\n```\n\nJust to reiterate, **this is a breaking change, that will impact you** if you're currently using metadata attributes. I may add an analyzer to try to warn about this potential issue in a subsequent release, which brings us to the next category: analyzers\n\n[New analyzers to warn of incorrect usage](#new-analyzers-to-warn-of-incorrect-usage)\n\nThere are several scenarios in which the code generated by the NetEscapades.EnumGenerators package won't compile. These are often edge cases that are tricky to handle in the generator, but which can be very confusing if you hit them in your application.\n\nTo work around the issue, I added several Roslyn analyzers to explain and warnabout cases that will cause problems.\n\n[Flagging generated extension class name clashes](#flagging-generated-extension-class-name-clashes)\n\nCurrently, you can decorate `enum`\n\ns with `[EnumExtension]`\n\nattributes in such a way that the same extension class name is used in both cases, which causes name clashes. For example, the following generates `SomeNamespace.MyEnumExtensions`\n\ntwice, one for each `enum`\n\n:\n\n```\nnamespace SomeNamespace;\n\n[EnumExtensions]\npublic enum MyEnum\n{\n    One,\n    Two\n}\n\npublic class Nested\n{\n    [EnumExtensions]\n    public enum MyEnum\n    {\n        One,\n        Two\n    }\n}\n```\n\n*Ideally* we would disambiguate by generating `SomeNamespace.Nested.MyEnumExtensions`\n\nas a nested class for the second case, but unfortunately extension method classes *can't* be nested classes.\n\nAnother option would be to include the class name in the generated namespace, but then that runs into *another* issue that can generate clashes. Ultimately, there's always a way to get clashes, especially as you can explicitly set the name of the class to generate!\n\nGiven that these types of clashes are going to be very rare, [#158 added](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/158) an analyzer, with diagnostic ID `NEEG001`\n\n, which flags the fact there's a clash on the `[EnumExtensions]`\n\nattribute directly as an error diagnostic.\n\nThis isn't strictly necessary, because generating duplicate extension classes results in a *lot* of compiler errors, but having an analyzer will hopefully make it more obvious exactly what's happened. 😅\n\n[Handling enums nested in generic types](#handling-enums-nested-in-generic-types)\n\nAnother case where we simply *can't* generate valid code is if you have an enum nested inside a generic type:\n\n```\nusing NetEscapades.EnumGenerators;\n\npublic class Nested<T> // Type is generic\n{\n    [EnumExtensions]\n    public enum MyEnum // Enum is nested inside\n    {\n        First,\n        Second,\n    }\n}\n```\n\nUnfortunately there's no easy way to generate a valid extension class in this case. We can't put the generated extension class inside `Nested<T>`\n\n, because extension methods can't be inside nested types. There's some things we *could* do with making the extension class itself generic, but that's all a bit confusing and opens the flood gates to some complexity.\n\nInstead, [in #159](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/159) I opted to just not support this scenario. If you write code like the above, no extension method is generated, and instead the `NEEG002`\n\ndiagnostic is applied to the `[EnumExtensions]`\n\nattribute to warn you that this isn't valid.\n\n[Duplicate case labels in an enum](#duplicate-case-labels-in-an-enum)\n\nThe final analyzer added in this release handles the case where you have \"duplicate\" enum members, that is, enum members with the same \"value\" as others. For example in the code below, both `Failed`\n\nand `Error`\n\nhave the same value:\n\n```\n[EnumExtensions]\npublic enum Status\n{\n    Unknown = 0,\n    Pending = 1,\n    Failed = 2,\n    Error = 2,\n}\n```\n\nThis is perfectly valid, but due to the way the enum generator works with switch expressions, it means you won't always get the value you expect if you call `ToStringFast()`\n\n(or other methods). This isn't an issue with the generator *per se*, as you see similar behaviour using the built-in `ToString()`\n\nmethod:\n\n``` js\nvar status = Status.Error;\nConsole.WriteLine(status.ToString()); // prints Failed\n```\n\nThis is just an artifact of how `enum`\n\ns work behind the scenes in .NET, but it can be confusing, so [#162](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/162) adds an analyzer that flags these problematic cases with a diagnostic `NEEG003`\n\n:\n\n```\n[EnumExtensions]\npublic enum Status\n{\n    Unknown = 0,\n    Pending = 1,\n    Failed = 2,\n    Error = 2,  // NEEG003: Enum has duplicate values and will give inconsistent values for ToStringFast()\n}\n```\n\nThis diagnostic is just `Info`\n\n, so it won't break your build, as it's still *valid* to use `[EnumExtensions]`\n\nwith these cases, it's just important to be aware that the generated extensions *might* not work as you expect!\n\nThat covers all the new analyzers, so finally we'll look at some of the fixes.\n\n[Bug fixes](#bug-fixes)\n\nThe first fix, introduced [in #165](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/165) and then fixed *properly* [in #172](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/172) was to better handle the cases where users have set their project's `LangVersion`\n\nto `Preview`\n\n.\n\nIn a previous release of NetEscapades.EnumGenerators [I added support for C#14 Extension Members](/exploring-dotnet-10-preview-features-3-csharp-14-extensions-members/#a-case-study-netescapades-enumgenerators). This lets you call static extension members as though they're defined on the type itself. For example, lets say you have this `enum`\n\n:\n\n```\n[EnumExtensions]\npublic enum MyColours\n{\n    Red,\n    Green,\n    Blue,\n}\n```\n\nThe source generator generates a `MyColoursExtensions.Parse()`\n\nmethod, but with extension members, you can call it as though it's defined on the `MyColours`\n\nenum itself:\n\n``` js\nvar colour = MyColours.Parse(\"Red\");\n```\n\nI intended to only enable this when you're using C#14, but I made a mistake. I enabled it when you're using C#14 *or* when you've set the `LangVersion=Preview`\n\n. Long story short, `Preview`\n\ncan mean practically anything depending on what you're targeting and what version of the SDK you're building with, so this was not a good idea 😅\n\nAs a fix, I removed the generation of extension members unless you're explicitly targeting C#14 or higher (*ignoring* the `Preview`\n\ncase). To allow opt-in to extension members when you're using `Preview`\n\n, I added a `EnumGenerator_ForceExtensionMembers`\n\nsetting that you can set to `true`\n\nto explicitly opt-in when you wouldn't normally. Unfortunately I accidentally initially *defaulted* this to `true`\n\n, so [#172](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/172) fixes this to be `false`\n\nby default instead 🙈\n\nThe main other fix was for handling the case where [enum member names are reserved words](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/166), e.g.\n\n```\n[EnumExtensions]\npublic enum AttributeFieldType\n{\n    number,\n    @string, // reserved, so escaped with @\n    date\n}\n```\n\nUnfortunately, I wasn't handling this correctly, so the generator was generating invalid code:\n\n``` js\npublic static string ToStringFast(this AttributeFieldType value)\n    => value switch\n    {\n        global::AttributeFieldType.number => \"number\",\n        global::AttributeFieldType.string => \"string\", // ❌ Does not compile\n        global::AttributeFieldType.date => \"date\",\n        _ => value.AsUnderlyingType().ToString(),\n    };\n```\n\nThe fix involved updating the generator with this handy function, to make sure we correctly escape the identifiers as necessary:\n\n```\nprivate static string EscapeIdentifier(string identifier)\n{\n    return SyntaxFacts.GetKeywordKind(identifier) != SyntaxKind.None\n        ? \"@\" + identifier\n        : identifier;\n}\n```\n\nSo that the generated code is escaped correctly:\n\n``` js\npublic static string ToStringFast(this AttributeFieldType value)\n    => value switch\n    {\n        global::AttributeFieldType.number => \"number\",\n        global::AttributeFieldType.@string => \"string\", // ✅ Correctly escaped\n        global::AttributeFieldType.date => \"date\",\n        _ => value.AsUnderlyingType().ToString(),\n    };\n```\n\nThe final change was to remove the `NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES`\n\noption in [#160](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/160) which removes the ability to embed the marker attributes in the target dll. This is rarely the right thing to do, and the package is already doing the work to ship the attribute in a dedicated dll. This also reduces some of the duplication, removes a config combination to need to test, and opens up the ability to ship \"helper\" types in the \"attributes\" dll in the future.\n\n[Summary](#summary)\n\nIn this post I walked through some of the recent updates to [NetEscapades.EnumGenerators](https://github.com/andrewlock/NetEscapades.EnumGenerators) shipped in version 1.0.0-beta16. These quality of life updates add support for `[EnumMember]`\n\n, updates how metadata attributes are used, and adds additional analyzers to catch potential pitfalls. Finally it fixes a few edge-case bugs. If you haven't already, I recommend updating and giving it a try! If you run into any problems, please do [log an issue on GitHub](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues).🙂", "url": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-enummember-support-analyzers-and", "canonical_source": "https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/", "published_at": "2025-12-02 09:00:00+00:00", "updated_at": "2026-05-23 21:41:07.315175+00:00", "lang": "en", "topics": ["developer-tools", "open-source"], "entities": ["NetEscapades.EnumGenerators", ".NET"], "alternates": {"html": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-enummember-support-analyzers-and", "markdown": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-enummember-support-analyzers-and.md", "text": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-enummember-support-analyzers-and.txt", "jsonld": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-enummember-support-analyzers-and.jsonld"}}