{"slug": "recent-updates-to-netescapades-enumgenerators-new-apis-and-system-memory-support", "title": "Recent updates to NetEscapades.EnumGenerators: new APIs and System.Memory support", "summary": "The article summarizes updates to version 1.0.0-beta19 of the NetEscapades.EnumGenerators NuGet package, a source generator that creates fast methods for working with enums by replacing slow built-in operations like `ToString()` with optimized switch statements. New features include the ability to disable number parsing in `Parse()` and `TryParse()` methods, support for automatically applying `ToLowerInvariant()` or `ToUpperInvariant()` to serialized enums, and added `ReadOnlySpan<T>` API support when using the System.Memory package.", "body_md": "In this post I describe some of the recent updates added in version 1.0.0-beta19 of 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 briefly describing why the package exists and what you can use it for, then I walk through some of the changes in the latest release.\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) provides a source generator that is designed to work around an annoying characteristic of working with enums: some operations are surprisingly slow.\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 for simplicity of 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 the `ToStringFast()`\n\nimplementation is, even in .NET 10\n\n| Method | Mean | Error | StdDev | Median | Gen0 | Allocated |\n|---|---|---|---|---|---|---|\n| ToString | 6.4389 ns | 0.1038 ns | 0.0971 ns | 6.4567 ns | 0.0038 | 24 B |\n| ToStringFast | 0.0050 ns | 0.0202 ns | 0.0189 ns | 0.0000 ns | - | - |\n\nObviously your mileage may vary and the results will depend on the specific enum and which of the generated methods 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 all the features the package provides, check my\n\n[previous blog posts]or see the project[README].\n\nThat's the basics of why I think you should take a look at the source generator. Now let's take a look at the latest features added.\n\n[Updates in 1.0.0-beta19](#updates-in-1-0-0-beta19)\n\nVersion [1.0.0-beta19 of NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators/) was released to nuget.org recently and includes a number of new features. I'll describe each of the updates in more detail below, covering the following:\n\n- Support for disabling number parsing.\n- Support for automatically calling\n`ToLowerInvariant()`\n\nor`ToUpperInvariant()`\n\non the serialized enum. - Add support for\n`ReadOnlySpan<T>`\n\nAPIs when using[the](https://www.nuget.org/packages/System.Memory).`System.Memory`\n\nNuGet package\n\nThere are other fixes and features in 1.0.0-beta19, but these are the ones I'm focusing on in this post. I'll show some of the other features in the next post!\n\n[Support for disabling number parsing and additional options](#support-for-disabling-number-parsing-and-additional-options)\n\nThe first feature addresses a [long-standing request](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/80), disabling the fallback \"number parsing\" implemented in `Parse()`\n\nand `TryParse()`\n\n. For clarity I'll provide a brief example. Let's take the `Color`\n\nexample again:\n\n```\n[EnumExtensions]\npublic enum Colour\n{\n    Red = 0,\n    Blue = 1,\n}\n```\n\nAs well as `ToStringFast()`\n\n, we generate similar \"fast\" `Parse()`\n\n, `TryParse()`\n\n, and most of the other `System.Enum`\n\nstatic methods that you might expect. The `TryParse()`\n\nmethod for the above code looks something like this:\n\n```\npublic static bool TryParse(string? name, out Colour value)\n{\n    switch (name)\n    {\n        case nameof(Colour.Red):\n            value = Colour.Red;\n            return true;\n        case nameof(Colour.Blue):\n            value = Colour.Blue;\n            return true;\n        case string s when int.TryParse(name, out var val):\n            value = (Colour)val;\n            return true;\n        default:\n            value = default;\n            return false;\n    }\n}\n```\n\nThe first two branches in this example are what you might expect; the source generator generates an explicit switch statement for the `Colour`\n\nenum. However, the third case may look a little odd. The problem is that `enum`\n\ns in C# are not a closed list of values, you can always do something like this:\n\n```\nColour valid = (Colour)123;\nstring stillValid = valid.ToString(); // \"123\"\nColour parsed = Enum.Parse<Colour>(stillValid); // 123\nConsole.WriteLine(valid == parsed); // true\n```\n\nEssentially, you can pretty much parse *any* integer that has been `ToString()`\n\ned as *any* enum. That's why the source generated code includes the case statement to try parsing as an integer—it's to ensure compatibility with the \"built-in\" `Enum.Parse()`\n\nand `TryParse()`\n\nbehaviour.\n\nHowever, that behaviour isn't always what you want. Arguably, it's *rarely* what you want, hence [the request](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/80) to allow disabling it.\n\nGiven it makes so much sense to allow disabling it, it's perhaps a little embarrassing that it took so long to address. I dragged my feet on it for several reasons:\n\n- The built-in\n`System.Enum`\n\nworks like this, and I want to be as drop-in compatible as possible. - There's a trivial \"fix\" by checking first that the first digit is not a number. e.g.\n`if (!char.IsDigit(text[0]) && ColourExtensions.TryParse(text, out var value))`\n\n- It's\n*another*configuration switch that would need to be added to`Parse`\n\nand`TryParse`\n\nOf all the reasons, that latter point is the one that vexed me. There were already two configuration knobs for `Parse`\n\nand `TryParse`\n\n, as well as versions that accept both `string`\n\nand `ReadOnlySpan<char>`\n\n:\n\n```\npublic static partial class ColourExtensions\n{\n    public static Colour Parse(string? name);\n    public static Colour Parse(string? name, bool ignoreCase);\n    public static Colour Parse(string? name, bool ignoreCase, bool allowMatchingMetadataAttribute);\n\n    public static Colour Parse(ReadOnlySpan<char> name);\n    public static Colour Parse(ReadOnlySpan<char> name, bool ignoreCase);\n    public static Colour Parse(ReadOnlySpan<char> name, bool ignoreCase, bool allowMatchingMetadataAttribute);\n}\n```\n\nMy concern was adding more and more parameters or overloads would make the generated API harder to understand. The simple answer was to take the classic approach of introducing an \"options\" object. This encapsulates all the available options in a single parameter, and can be extended without increasing the complexity of the generated APIs:\n\n```\npublic static partial class ColourExtensions\n{\n    public static Colour Parse(string? name, EnumParseOptions options);\n    public static Colour Parse(ReadOnlySpan<char> name, EnumParseOptions options);\n}\n```\n\n[The EnumParseOptions object](https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/master/src/NetEscapades.EnumGenerators.Attributes/EnumParseOptions.cs) is defined in the source generator dll that's referenced by your application, so it becomes part of the public API. It's defined as a\n\n`readonly struct`\n\nto avoid allocating an object on the heap just to call `Parse`\n\n(which would negate some of the benefits of these APIs).\n\n```\npublic readonly struct EnumParseOptions\n{\n    private readonly StringComparison? _comparisonType;\n    private readonly bool _blockNumberParsing;\n\n    public EnumParseOptions(\n        StringComparison comparisonType = StringComparison.Ordinal,\n        bool allowMatchingMetadataAttribute = false,\n        bool enableNumberParsing = true)\n    {\n        _comparisonType = comparisonType;\n        AllowMatchingMetadataAttribute = allowMatchingMetadataAttribute;\n        _blockNumberParsing = !enableNumberParsing;\n    }\n\n    public StringComparison ComparisonType => _comparisonType ?? StringComparison.Ordinal;\n    public bool AllowMatchingMetadataAttribute { get; }\n    public bool EnableNumberParsing => !_blockNumberParsing;\n}\n```\n\nThe main difficulty in designing this object was that the default values (i.e. `(EnumParseOptions)default`\n\n) had to match the \"default\" values used in other APIs, i.e.\n\n*Not*case sensitive- Number parsing\n*enabled* *Don't*match metadata attributes (see my[previous post](/recent-updates-to-netescapaades-enumgenerators/)about recent changes to metadata attributes!).\n\nThe final object ticks all those boxes, and it means you can now disable number parsing, using code like this:\n\n```\nstring someNumber = \"123\";\nColourExtensions.Parse(someNumer, new EnumParseOptions(enableNumberParsing: false)); // throws ArgumentException\n```\n\nAs well as introducing number parsing, this also provided a way to sneak in the ability to use *any* type of `StringComparison`\n\nduring `Parse`\n\nor `TryParse`\n\nmethods, instead of only supporting `Ordinal`\n\nand `OrdinalIgnoreCase`\n\n.\n\n[Support for automatic ](#support-for-automatic-tolowerinvariant-calls)`ToLowerInvariant()`\n\ncalls\n\n`ToLowerInvariant()`\n\ncallsThe [next feature](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/177) was *also* [a feature request that's been around for a long time](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/36), the ability to do the equivalent of `ToString().ToLowerInvariant()`\n\nbut without the intermediate allocation, and with the transformation done at compile time, essentially generating something similar to these:\n\n```\npublic static class ColourExtensions\n{\n    public static string ToStringLowerInvariant(this Colour colour) => colour switch\n    {\n        Colour.Red => \"red\",\n        Colour.Blue => \"blue\",\n        _ => colour.ToString().ToLowerInvariant(),\n    };\n\n    public static string ToStringUpperInvariant(this Colour colour) => colour switch\n    {\n        Colour.Red => \"RED\",\n        Colour.Blue => \"BLUE\",\n        _ => colour.ToString().ToUpperInvariant(),\n    }\n}\n```\n\nThere are various reasons you might want to do that, for example if third-party APIs require that you use upper/lower for your enums, but you want to keep your definitions as canonical C# naming. This was another case where I could see the value, but I didn't really *want* to add it, as it looked like it would be a headache. `ToStringLower()`\n\nis kind of ugly, and there would be a bunch of extra overloads required again.\n\nJust as for the number parsing scenario, the solution I settled on was to add [a SerializationOptions object](https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/master/src/NetEscapades.EnumGenerators.Attributes/SerializationOptions.cs#L6) that supports a\n\n`SerializationTransform`\n\n, which can potentially be extended in the future if required (though I'm not chomping at the bit to add more options right now!)\n\n```\npublic readonly struct SerializationOptions\n{\n    public SerializationOptions(\n        bool useMetadataAttributes = false,\n        SerializationTransform transform = SerializationTransform.None)\n    {\n        UseMetadataAttributes = useMetadataAttributes;\n        Transform = transform;\n    }\n\n    public bool UseMetadataAttributes { get; }\n    public SerializationTransform Transform { get; }\n}\n\npublic enum SerializationTransform\n{\n    None,\n    LowerInvariant,\n    UpperInvariant,\n}\n```\n\nYou can then use the `ToStringFast()`\n\noverload that takes a `SerializationOptions`\n\nobject, and it will output the lower version of your enum, without needing the intermediate `ToStringFast`\n\ncall:\n\n``` js\nvar colour = Colour.Red;\nConsole.WriteLine(colour.ToStringFast(new(transform: SerializationTransform.LowerInvariant))); // red\n```\n\nIt's not the tersest of syntax, but there's ways to clean that up, and it means that adding additional options later if required should be less of an issue, but we shall see. In the short-term, it means that you can now use this feature if you find yourself needing to call `ToLowerInvariant()`\n\nor `ToUpperInvariant()`\n\non your enums.\n\n[Support for the ](#support-for-the-system-memory-nuget-package)*System.Memory* NuGet package\n\n*System.Memory*NuGet package\n\nFrom the start, NetEscapades.EnumGenerators has had support for parsing values from `ReadOnlySpan<char>`\n\n, just like the modern APIs in `System.Enum`\n\ndo (but faster 😉). However, these APIs have always been guarded by a pre-processor directive; if you're using .NET Core 2.1+ or .NET Standard 2.1 then they're available, but if you're using .NET Framework, or .NET Standard 2.0, then they're not available.\n\n```\n#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER\n    public static bool IsDefined(in ReadOnlySpan<char> name);\n    public static bool IsDefined(in ReadOnlySpan<char> name, bool allowMatchingMetadataAttribute);\n    public static Colour Parse(in ReadOnlySpan<char> name);\n    public static bool TryParse(in ReadOnlySpan<char> name, out Colour colour);\n    // etc\n#endif\n```\n\nIn my experience, this isn't a *massive* problem these days. If you're targeting any version of .NET Core or modern .NET, then you have the APIs. If, on the other hand, you're on .NET Framework, then the speed of `Enum.ToString()`\n\nwill really be the least of your performance worries, and the lack of the APIs probably don't matter *that* much.\n\nWhere it *could* still be an issue is .NET Standard 2.0. It's not recommended to use this target if you're creating libraries for .NET Core or modern .NET, but if you need to target *both* .NET Core and .NET Framework, then you don't necessarily have a lot of choice, you need to use .NET Standard.\n\nWhat's more, there's a [ System.Memory NuGet package](https://www.nuget.org/packages/System.Memory) that provides polyfills for\n\n*many*of the APIs, in particular\n\n`ReadOnlySpan<char>`\n\n.As I understand it the polyfill implementation isn't generally\n\nasfast as the built-in version, but it's still something of an improvement!\n\nSo the new feature in 1.0.0-beta19 of NetEscapades.EnumGenerators is that you can define an MSBuild property, `EnumGenerator_UseSystemMemory=true`\n\n, and the `ReadOnlySpan<char>`\n\nAPIs will be available where previously they wouldn't be. Note that you *only* need to define this if you're targeting .NET Framework or .NET Standard 2.0 and want the `ReadOnlySpan<char>`\n\nAPIs.\n\n```\n<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>netstandard2.0</TargetFrameworks>\n    <!-- 👇Setting this in a .NET Standard 2.0 project enables the ReadOnlySpan<char> APIs-->\n    <EnumGenerator_UseSystemMemory>true</EnumGenerator_UseSystemMemory>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"NetEscapades.EnumGenerators\" Version=\"1.0.0-beta19\" />\n    <PackageReference Include=\"System.Memory\" Version=\"4.6.3\" />\n  </ItemGroup>\n</Project>\n```\n\nSetting this property defines a constant, `NETESCAPADES_ENUMGENERATORS_SYSTEM_MEMORY`\n\n, which the updated generated code similarly predicates on:\n\n```\n                                                          // 👇 New in 1.0.0-beta19\n#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER || NETESCAPADES_ENUMGENERATORS_SYSTEM_MEMORY\n    public static bool IsDefined(in ReadOnlySpan<char> name);\n    public static bool IsDefined(in ReadOnlySpan<char> name, bool allowMatchingMetadataAttribute);\n    public static Colour Parse(in ReadOnlySpan<char> name);\n    public static bool TryParse(in ReadOnlySpan<char> name, out Colour colour);\n    // etc\n#endif\n```\n\nThere *are* some caveats:\n\n- The\n*System.Memory*package doesn't provide an implementation of`int.TryParse()`\n\nthat works with`ReadOnlySpan<char>`\n\n, so:*If*you're attempting to parse a`ReadOnlySpan<char>`\n\n,*and*the`ReadOnlySpan<char>`\n\n*doesn't*represent one of your enum types*and*you haven't disabled number parsing*then*the APIs will potentially allocate, so that they can call`int.TryParse(string)`\n\n.\n\n- Additional warnings are included in the XML docs to warn about the above scenario\n- If you set the variable, and\n*haven't*added a reference to*System.Memory*, you'll get compilation warnings.\n\nAs part of shipping the feature, I've also tentatively added \"detection\" of referencing the *System.Memory* NuGet package in the package `.targets`\n\nfile, so that `EnumGenerator_UseSystemMemory=true`\n\nshould be *automatically* set, simply by referencing *System.Memory*. However, I consider this part somewhat experimental, as it's not something I've tried to do before, I'm not sure it's something you *should* do, and I'm a long way from thinking it'll work 100% of the time 😅 I'd be interested in feedback on how I *should* do this, and/or whether it works for you!\n\nI also experimented with a variety of other approaches. Instead of using a defined constant, you could also detect the availability of\n\n`ReadOnlySpan<char>`\n\nin the generator itself, and emit different code entirely. But you'd still need to detect whether the`int.TryParse()`\n\noverloads are available (i.e. is`ReadOnlySpan<char>`\n\n\"built in\" or package-provided), and overall it seemed way more complex to handle than the approach I settled on. I'm still somewhat torn though, and maye revert to this approach in the future. And I'm open to other suggestions!\n\nSo in summary, if you're using NetEscapades.EnumGenerators in a `netstandard2.0`\n\nor `.NET Framework`\n\npackage, and you're already referencing *System.Memory*, then *in theory* you should magically get additional `ReadOnlySpan<char>`\n\nAPIs by updating to 1.0.0-beta19. If that's *not* the case, then I'd love if you could [raise an issue](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues) so we can understand why, but you can also simply set `EnumGenerator_UseSystemMemory=true`\n\nto get all that performance goodness guaranteed.\n\nBefore I close, I'd like to say a big thank you to everyone who has raised issues and PRs for the project, especially [Paulo Morgado](https://github.com/paulomorgado) for his discussion and work around the *System.Memory* feature! All feedback is greatly appreciated, so do [raise an issue](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues) if you have any problems.\n\n[When is a non-pre-release 1.0.0 release coming?](#when-is-a-non-pre-release-1-0-0-release-coming-)\n\nSoon. I promise 😅 So give the new version a try and flag any issues so they can be fixed before we settle on the final API for once and for all! 😀\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-beta18. I showed how introducing options objects for both the `Parse()`\n\n/`TryParse()`\n\nand `ToString()`\n\nmethods allowed introducing new features such as disabling number parsing and serializing directly with `ToLowerInvariant()`\n\n. Finally, I showed the new support for `ReadOnlySpan<char>`\n\nAPIs when using .NET Framework or .NET Standard 2.0 with [the System.Memory NuGet package](https://www.nuget.org/packages/System.Memory) If you haven't already, I recommend updating and giving it a try! If you run into any problems, please do\n\n[log an issue on GitHub](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues).🙂", "url": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-new-apis-and-system-memory-support", "canonical_source": "https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/", "published_at": "2026-01-02 09:00:00+00:00", "updated_at": "2026-05-23 21:40:06.819944+00:00", "lang": "en", "topics": ["developer-tools", "open-source"], "entities": ["NetEscapades.EnumGenerators", "NuGet"], "alternates": {"html": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-new-apis-and-system-memory-support", "markdown": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-new-apis-and-system-memory-support.md", "text": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-new-apis-and-system-memory-support.txt", "jsonld": "https://wpnews.pro/news/recent-updates-to-netescapades-enumgenerators-new-apis-and-system-memory-support.jsonld"}}