# Splitting the NetEscapades.EnumGenerators packages: the road to a stable release

> Source: <https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/>
> Published: 2026-03-10 09:00:00+00:00

In this post I describe some of the significant restructuring 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`

s. I start by describing why the package exists and what you can use it for, then I describe what motivated the restructuring, and finally what the changes are and a call to action.

The

tl;dr;is that there are nowthreedifferent packages, and exactly which one is best for you depends on what you're trying to do. Check[the section below]or[the project's README]for details!

As an aside, I *really* want to give this package a stable 1.0.0 release shortly, as I think we've solved most of the corner cases that were bugging me. Which means if the new package structure *doesn't* work for you, now is the time to [raise an issue](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues), before it gets set in stone!

[Why should you use an enum source generator?](#why-should-you-use-an-enum-source-generator-)

[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.

Note that while this has

historicallybeen true, this fact won't necessarily remain true forever. In fact, .NET 8+ provided a bunch of improvements to enum handling in the runtime.

As an example, let's say you have the following enum:

```
public enum Colour
{
    Red = 0,
    Blue = 1,
}
```

At some point, you want to print the name of a `Color`

variable, so you create this helper method:

```
public void PrintColour(Colour colour)
{
    Console.WriteLine("You chose "+ colour.ToString()); // You chose Red
}
```

While 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()`

method that looks something 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 simple switch statement checks for each of the known values of `Colour`

and uses `nameof`

to return the textual representation of the `enum`

. If it's an unknown value, then it falls back to the built-in `ToString()`

implementation to ensure correct handling of unknown values (for example this is valid C#: `PrintColour((Colour)123)`

).

If we compare these two implementations using [BenchmarkDotNet](https://benchmarkdotnet.org/) for a known colour, you can see how much faster `ToStringFast()`

implementation is:

| 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 | - | - |
| ToString | `net10.0` | 6.4389 ns | 0.1038 ns | 0.0971 ns | 1.000 | 0.0038 | 24 B |
| ToStringFast | `net10.0` | 0.0050 ns | 0.0202 ns | 0.0189 ns | 0.001 | - | - |

Even though recent versions of .NET are way faster, the overall pattern hasn't changed: .NET is *way* faster than .NET Framework, and the `ToStringFast()`

implementation is way faster than the built-in `ToString()`

. 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.

That's the basics of the package, for more details see [the project's README](https://github.com/andrewlock/NetEscapades.EnumGenerators/). In the next section I describe how adding some new features managed to break users, and what we did in response.

[Adding new features by adding to the marker attribute dll](#adding-new-features-by-adding-to-the-marker-attribute-dll)

In version `1.0.0-beta19`

of *NetEscapades.EnumGenerators* [I introduced a bunch of new features ](/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/)that had been [long standing](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/80) [requests](https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/177):

- Support for disabling number parsing.
- Support for automatically calling
`ToLowerInvariant()`

or`ToUpperInvariant()`

on the serialized enum.

There wasn't really a *technical* reason I took so long to add these features. The problem was that I didn't want to add dozens of different overloads of `Enum.Parse()`

or `ToString()`

to accommodate all the different possible options. Similarly, I didn't want to add these all as different (IMO, ugly) additional extension methods.

I solved this issue in what I *thought* was a neat way. When you referenced the *NetEscapades.EnumGenerators* package, your library references the *NetEscapades.EnumGenerators.Attributes.dll* file that is shipped in the package:

This is just *one* way to add "marker" attributes to a target application, and [until recently](/exploring-dotnet-10-preview-features-4-solving-the-source-generator-marker-attribute-problem-in-dotnet-10/) it was the most reliable way. My epiphany was to realise that I could put *other* types in this dll too; including types that are part of the target app's "public" API.

So I added `EnumParseOptions`

and `SerializationOptions`

types to *NetEscapades.EnumGenerators.Attributes.dll*, and used these types to add "general, extensible" overloads for the generated `Parse`

and `ToString()`

methods, something like this:

```
public static partial class ColourExtensions
{
    // EnumParseOptions controls case sensitivity, number parsing, matching metadata attributes 
    public static Colour Parse(string? name, in EnumParseOptions options);

    // SerializationOptions controls case transforms and whether to use metadata attributes
    public static string ToString(Color valu, in SerializationOptions options)
}
```

This all seemed very neat. If we want to add more features, we can just extend the options objects, no need for new methods or anything. However, I inadvertently managed to break a bunch of users 🤦♂️

[When new features break users…](#when-new-features-break-users)

Shortly after publishing the new version of the package, I [received reports](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/231) of users seeing the following error:

```
Error CS0012: The type 'EnumParseOptions' is defined in an assembly that is not referenced. You must add a reference to assembly 'NetEscapades.EnumGenerators.Attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. (184, 11)
```

The issue seemed pretty clear: the *NetEscapades.EnumGenerators.Attributes.dll* that contains the new options objects used in the API wasn't being referenced in the final application. As I said in my [initial response](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/231#issuecomment-3716473001):

I'm guessing that you're excluding assets where you reference the package, e.g. something like this?

```
<PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta19" > PrivateAssets="all" />
```

or like this:

```
<PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta19" ExcludeAssets="all" />
```

you can't do that, you need just a normal reference:

```
<PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta19" />
```

But what I totally overlooked is that many people *intentionally* add references to source generators using this pattern:

For all our code generators, we list them in the Directory.Build.props at the solution level as such:

```
<PackageReference Include="NetEscapades.EnumGenerators"
                  ExcludeAssets="runtime"
                  PrivateAssets="all"
                  TreatAsUsed="true"/>
```

We do this so that every project has access to code generation, yet doesn't pass on the generator assemblies themselves as transitive dependencies. Our expectation is such that when we distribute our packages, we don't impose upon our consumers any unnecessary dependencies through transitive references (i.e. available in everything, pass it onto nothing).

Unfortunately for me, that makes *total* sense 😅

For some reason, I thought that source generators (and analyzers) *didn't* flow transitively to downstream projects. But that's not true, they *do* flow transitively. To make that clearer, imagine you have two projects, `MyProject.Lib`

and `MyProject.Web`

. `MyProject.Web`

has a reference to `MyProject.Lib`

, and `MyProject.Lib`

adds a reference to *NetEscapades.EnumGenerators*. By default, `MyProject.Web`

**also** gets a transitive reference to *NetEscapades.EnumGenerators*.

```
     [MyProject.Lib]          ←              [MyProject.Web]
            ↓                                      (↓)  
[NetEscapades.EnumGenerators]           [NetEscapades.EnumGenerators]
```

This is how "normal" project/package references work, but for some reason I thought source generators/analyzers were special. But it turns out no.

So why does this matter? Well it means that adding `PrivateAssets="all"`

and `ExcludeAssets="runtime"`

actually makes sense in a *lot* of cases, particularly if you're creating reusable libraries. If you're using a generator to generate code in a single package, and you *don't* want to force downstream consumers of your package to automatically be opted in to source generator, then these attributes can help, though they serve slightly different reasons:

`PrivateAssets="all"`

ensures that any projects referencing this project*don't*get the dependency as a transitive dependency. In this example above, if`MyProject.Lib`

set`PrivateAssets="all"`

, then`MyProject.Web`

*wouldn't*get a transitive reference to*NetEscapades.EnumGenerators*.`ExcludeAssets="runtime"`

ensures that any runtime assets included in the package are*not*copied to the build output. In our case, that means the*NetEscapades.EnumGenerators.Attributes.dll*would*not*be copied to the build output.

So on that basis, it's clear that adding `ExcludeAssets="runtime"`

was causing the missing reference error at run time. Previously the attribute only contained enum attributes, and those were [elided by default anyway](https://github.com/andrewlock/NetEscapades.EnumGenerators?tab=readme-ov-file#preserving-usages-of-the-enumextensions-attribute), so the "runtime dependencies" were actually just a benign spandrel that could be excluded without issue. But when I added the

`EnumParseOptions`

and `SerializationOptions`

, suddenly there was a hard dependency and things went boom 💥[The solution: more packages](#the-solution-more-packages)

Now, the "easy" fix would be to just tell users they're holding it wrong, and to just not use `ExcludeAssets="runtime"`

but there were some [very reasonable explanations](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/231#issuecomment-3719300265) that users had for not wanting to do this.

One of the most compelling reasons was that the *NetEscapades.EnumGenerators* source generator is *entirely* an "implementation detail". *Forcing* additional downstream runtime dependencies on consumers just to be "allowed" to use the source generator doesn't feel right. Especially if you're not even *using* the `EnumParseOptions`

or `SerializationOptions`

overloads!

After some various discussion and back and forth, we settled on two key changes:

- Move the "runtime dependencies" assembly (which currently only contains
`EnumParseOptions`

and`SerializationOptions`

) into a separate package,*NetEscapades.EnumGenerators.RuntimeDependencies*. *If*the`EnumParseOptions`

types are available in the compilation (because you added a reference to the runtime dependencies package) then the source generator uses the`EnumParseOptions`

and`SerializationOptions`

types in its public API. However, if they're*not*available, it emits`enum`

-specific versions of these types instead, which avoids the need to add the runtime dependencies package. More on this shortly!

In addition, to make onboarding easier, the actual source generator was split into a completely separate package, *NetEscapades.EnumGenerators.Generator*, and _*NetEscapades.EnumGenerators* becomes a metapackage instead:

```
NetEscapades.EnumGenerators
  |____NetEscapades.EnumGenerators.Generators
  |____NetEscapades.EnumGenerators.RuntimeDependencies
```

As discussed, these packages provide the following:

`NetEscapades.EnumGenerators`

is a meta package for easy install.`NetEscapades.EnumGenerators.Generators`

contains the source generator itself.`NetEscapades.EnumGenerators.RuntimeDependencies`

contains dependencies that need to be referenced at runtime by the generated code.

The default approach is to reference the meta-package in your project. The runtime dependencies and generator packages will then flow transitively to any project that references yours, and the generator will run in those projects by default. But by splitting these packages up, we get more flexibility.

[Avoiding runtime dependencies](#avoiding-runtime-dependencies)

As already discussed, in some cases you may not want these dependencies to flow to other projects, such as when you're using *NetEscapades.EnumGenerators* internally in your own library. In this scenario, you can take the following approach:

- Reference
directly, and set*NetEscapades.EnumGenerators.Generators*`PrivateAssets=All`

(and optionally`ExcludeAssets="runtime"`

) *Optionally*referencedirectly.*NetEscapades.EnumGenerators.RuntimeDependencies*

```
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <!-- 👇 Add the generator package with PrivateAssets -->
  <PackageReference Include="NetEscapades.EnumGenerators.Generators" Version="1.0.0-beta21" PrivateAssets="All"/>

  <!-- Optionally add the runtime dependencies package -->
  <PackageReference Include="NetEscapades.EnumGenerators.RuntimeDependencies" Version="1.0.0-beta21" />
</Project>
```

The [ NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies) package is a "normal" dependency that contains the

`EnumParseOptions`

, `SerializationOptions`

, and `SerializationTransform`

types:

```
namespace NetEscapades.EnumGenerators;

/// <summary>
/// Defines the options use when parsing enums using members provided by NetEscapades.EnumGenerator.
/// </summary>
public readonly struct EnumParseOptions { }

/// <summary>
/// Options to apply when calling <c>ToStringFast</c> on an enum.
/// </summary>
public readonly struct SerializationOptions

/// <summary>
/// Transform to apply when calling <c>ToStringFast</c>
/// </summary>
public enum SerializationTransform
```

However, if you *don't* add a reference to the [ NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies) package, the source generator creates "nested" versions of the dependencies in each generated extension method instead of using the "global" versions:

```
namespace SomeNameSpace;

public static partial class MyEnumExtensions
{
    // ... generated members

    // The runtime dependencies are generated as nested types instead
    public readonly struct EnumParseOptions { }
    public readonly struct SerializationOptions
    public enum SerializationTransform
}
```

Generating the runtime dependencies as nested types like this has both upsides and downsides:

- It avoids placing downstream dependency requirements on consumers of your library.
- You can still use these additional overloads internally, even without having additional runtime dependencies
- It makes consuming the APIs that use the runtime dependencies more verbose.

To make that last point concrete, if you add a reference to the *RuntimeDependencies* package, your code might look something like this:

```
// parsing
string serialized = "Red"
Color value = ColourExtensions.Parse(
    serialized, new EnumParseOptions(enableNumberParsing: false));

// serialization
var result = value.ToStringFast(
    new SerializationOptions(transform: SerializationTransform.LowerInvariant));
```

In contrast, if you omit the runtime dependencies, you have to use the full (nested) type names:

```
// parsing
string serialized = "Red"
Color value = ColourExtensions.Parse(
    serialized, new ColourExtensions.EnumParseOptions(enableNumberParsing: false));

// serialization
var result = value.ToStringFast(
    new ColourExtensions.SerializationOptions(transform: ColourExtensions.SerializationTransform.LowerInvariant));
```

which, you know, is pretty ugly. But it does avoid those runtime dependencies, and solves the users problems, so it's what is available today!

[Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario)

So where does that leave us?

In general, for simplicity, if you're creating an app of some sort I recommend just using a "normal" package reference to [ NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) (and thereby implicitly using

[). This particularly makes sense when you are the primary consumer of the extension methods, or where you don't mind if consumers end up referencing the generator package:](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies)

*NetEscapades.EnumGenerators.RuntimeDependencies*

```
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta21" />
</Project>
```

This "default" scenario also gets the best experience in terms of

[the optional "usage analyzers"], which flag places that you should consider calling`ToStringFast()`

.

In contrast, if you are producing a reusable library and don't want *any* runtime dependencies to be exposed to consumers, I recommend using [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) and setting `PrivateAssets=All`

and `ExcludeAssets="runtime"`

.

```
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <PackageReference Include="NetEscapades.EnumGenerators.Generators" Version="1.0.0-beta21" PrivateAssets="All" ExcludeAssets="runtime" />
</Project>
```

The final option is to reference [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) and set `PrivateAssets=All`

and `ExcludeAssets="runtime"`

(to avoid it being referenced transitively), but then *also* reference [NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies), to produce easier-to consume APIs.

```
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <PackageReference Include="NetEscapades.EnumGenerators.Generators" Version="1.0.0-beta21" PrivateAssets="All" ExcludeAssets="runtime"/>
  <PackageReference Include="NetEscapades.EnumGenerators.RuntimeDependencies" Version="1.0.0-beta21" />
</Project>
```

⚠️ When using the

[NetEscapades.EnumGenerators]metapackage, it's important youdon'tset`PrivateAssets=All`

. If you want to use`PrivateAssets=All`

or`ExcludeAssets="runtime"`

use[NetEscapades.EnumGenerators.Generators]for this scenario.

All of which means the package now supports many different scenarios, though with the associated complexity. All of which leads me to the final part of this post: how close are we to a stable release?

[Open questions before a stable release](#open-questions-before-a-stable-release)

I've been very hesitant to put out a stable `1.0.0`

release for the package, mostly because I don't really intend there to be a `2.0.0`

unless there's a *very* good reason. The reason being is that I want consumers of the package to not have to worry about things breaking underneath them.

I have a separate rant about how I feel many NuGet library authors have misunderstood the increased speed of the .NET ecosystem in recent years as a license to churn out major versions with endless breaking changes. But that's for a completely different post😅

In terms of features, I'm *pretty* happy with where the generator is today compared to even a few months ago:

- Optional
*automatic*support for System.Memory if it's available. - Optional usage analyzers to encourage
*using*the extension methods where possible. - The extensible
`EnumParseOptions`

and`SerializationOptions`

discussed in this post. - Catering to "internal" usages of the generator (where you don't want downstream consumers to be
*aware*you're using it). - Support for enums defined in
*other*libraries (including BCL enums)

Nevertheless, there are a few things I'm not entirely happy with. For example:

- The usage analyzers are IMO pretty cool, should they just be enabled as warnings by default?
- Presumably, if you're adding this package, you want to preferentially use them, which would suggest yes, enable them.
- But on the other hand, that would potentially be a big breaking change and annoy a bunch of people that just have a targeted usage
*However*they could just disable them in that case.- Unfortunately, you need to "preserve attribute usages" if you want the analyzers to work in "downstream" projects
*and*ensure you don't exclude runtime assets, but that's maybe just a limitation we'll have to live with s🤷♂️

- The nested runtime dependency types are pretty ugly
- Should they just be omitted/made private implementation details entirely?
- My concern is that while they're a neat solution to a problem, maybe they should just be omitted (or made
`private`

, depending on complexity) to reduce confusion. The onboarding story becomes easier in this scenario: "add the*NetEscapades.EnumGenerators.RuntimeDependencies*package and get these extra features".

- You can't use "target-typed"
`new`

on the`EnumParseOptions`

and`SerializationOptions`

overloads due to ambiguity with existing methods (shown below)- This is
*really*annoying, but I think the only way to "solve" it is to remove the overloads that contain the same number of parameters, which is again, pretty disruptive, and those methods are there for a reason (convenience)! - The biggest concern is
`EnumExtensions.Parse(string name, bool ignoreCase)`

, as that bool is more convenient than using`EnumExtensions.Parse(string name, EnumParseOptions options)`

if you*just*want to ignore case. - But maybe it's worth seriously entertaining making that break sooner rather than later.

- This is

Anyway, those are my current thoughts with the library. I need to figure out the answer to these questions before pushing out the 1.0.0, but I'd love for feedback from any users of the package that would be impacted by any of the changes above. Let me know your thoughts here or in [the issue on Github](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/67). And hopefully, *hopefully*, we can get a stable version soon 😅

In the mean time, do try out [the latest version](https://www.nuget.org/packages/NetEscapades.EnumGenerators/1.0.0-beta21), `1.0.0-beta21`

, read the [package notes](https://github.com/andrewlock/NetEscapades.EnumGenerators), and let me know about any issues or thoughts. There's extra goodness in there I haven't talked about in this post, so give it a try!

[Summary](#summary)

In this post I described the recent architectural to the *NetEscapades.EnumGenerators* package (which is now a metapackage) to support more scenarios. If you're a "standard" user of the package, there's nothing to worry about, you can simply add a reference to *NetEscapades.EnumGenerators* and everything should work smoothly.

However, if you want to control how the package flows transitively to consumers of your project then you may want to reference *NetEscapades.EnumGenerators.Generators* directly, and optionally add the *NetEscapades.EnumGenerators.RuntimeDependencies* package so you can use cleaner APIs.

I'd love any feedback you have about these recent changes; whether they're too confusing, if you can't work out which path you should take, or if you have any thoughts about the specific points I raised above. Just drop a comment here or in [the issue on Github](https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/67).
