From b08b41fa9081b097f268a3eedbe815113bb8997c Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Thu, 5 Mar 2026 21:35:54 +0000 Subject: [PATCH 1/7] Update to use trusted publishing --- .github/workflows/BuildAndPack.yml | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/workflows/BuildAndPack.yml b/.github/workflows/BuildAndPack.yml index 57e50ee4..979460e3 100644 --- a/.github/workflows/BuildAndPack.yml +++ b/.github/workflows/BuildAndPack.yml @@ -1,19 +1,3 @@ -# ------------------------------------------------------------------------------ -# -# -# This code was generated. -# -# - To turn off auto-generation set: -# -# [GitHubActions (AutoGenerate = false)] -# -# - To trigger manual generation invoke: -# -# nuke --generate-configuration GitHubActions_BuildAndPack --host GitHubActions -# -# -# ------------------------------------------------------------------------------ - name: BuildAndPack on: @@ -43,6 +27,9 @@ jobs: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: "true" name: ${{ matrix.os}} runs-on: ${{ matrix.vm}} + permissions: + id-token: write + contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 @@ -67,10 +54,17 @@ jobs: !~/.nuget/packages/netescapades.enumgenerators.interceptors key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj') }} + # Use the ambient GitHub token to login to NuGet and retrieve an API key + - name: NuGet login (OIDC → temp API key) + uses: NuGet/login@v1 + id: login + with: + user: ${{ secrets.NUGET_USER || 'NOT_SET' }} + - name: Run './build.cmd Clean Test TestPackage PushToNuGet run: ./build.cmd Clean Test TestPackage PushToNuGet env: - NuGetToken: ${{ secrets.NUGET_TOKEN || 'NOT_SET'}} + NuGetToken: ${{ steps.login.outputs.NUGET_API_KEY || 'NOT_SET'}} - uses: actions/upload-artifact@v4 with: From eacbd6adcc4ab4a2b3a6874e63e49b97be33884e Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 6 Mar 2026 21:45:36 +0000 Subject: [PATCH 2/7] Try to reduce duplication and differences in readmes --- .config/dotnet-tools.json | 13 + build/Build.cs | 6 + .../NetEscapades.EnumGenerators.Generators.md | 188 ++++++------- ...apades.EnumGenerators.Generators.source.md | 68 +++++ ...etEscapades.EnumGenerators.Interceptors.md | 18 +- ...ades.EnumGenerators.Interceptors.source.md | 66 +++++ ...ades.EnumGenerators.RuntimeDependencies.md | 13 + ...umGenerators.RuntimeDependencies.source.md | 0 .../NetEscapades.EnumGenerators.md | 235 +++++++++-------- docs/NetEscapades.EnumGenerators.source.md | 70 +++++ .../README.md | 53 ++-- docs/README.source.md | 70 +++++ docs/fragments/benchmark.include.md | 33 +++ docs/fragments/enum-usage.include.md | 246 ++++++++++++++++++ docs/fragments/interception-config.include.md | 55 ++++ docs/fragments/interceptor-intro.include.md | 18 ++ docs/fragments/package-referencing.include.md | 131 ++++++++++ docs/fragments/preserving-usages.include.md | 18 ++ docs/fragments/usage-analyzers.include.md | 69 +++++ docs/mdsnippets.json | 8 + ...Escapades.EnumGenerators.Generators.csproj | 2 +- ...capades.EnumGenerators.Interceptors.csproj | 2 +- ....EnumGenerators.RuntimeDependencies.csproj | 2 +- .../NetEscapades.EnumGenerators.csproj | 2 +- 24 files changed, 1157 insertions(+), 229 deletions(-) create mode 100644 .config/dotnet-tools.json rename src/NetEscapades.EnumGenerators.Generators/README.md => docs/NetEscapades.EnumGenerators.Generators.md (94%) create mode 100644 docs/NetEscapades.EnumGenerators.Generators.source.md rename src/NetEscapades.EnumGenerators.Interceptors/README.md => docs/NetEscapades.EnumGenerators.Interceptors.md (91%) create mode 100644 docs/NetEscapades.EnumGenerators.Interceptors.source.md create mode 100644 docs/NetEscapades.EnumGenerators.RuntimeDependencies.md rename src/NetEscapades.EnumGenerators.RuntimeDependencies/README.md => docs/NetEscapades.EnumGenerators.RuntimeDependencies.source.md (100%) rename README.md => docs/NetEscapades.EnumGenerators.md (79%) create mode 100644 docs/NetEscapades.EnumGenerators.source.md rename {src/NetEscapades.EnumGenerators => docs}/README.md (94%) create mode 100644 docs/README.source.md create mode 100644 docs/fragments/benchmark.include.md create mode 100644 docs/fragments/enum-usage.include.md create mode 100644 docs/fragments/interception-config.include.md create mode 100644 docs/fragments/interceptor-intro.include.md create mode 100644 docs/fragments/package-referencing.include.md create mode 100644 docs/fragments/preserving-usages.include.md create mode 100644 docs/fragments/usage-analyzers.include.md create mode 100644 docs/mdsnippets.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..b6c1d109 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "markdownsnippets.tool": { + "version": "28.0.1", + "commands": [ + "mdsnippets" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/build/Build.cs b/build/Build.cs index 51d99758..77a97f87 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -194,6 +194,12 @@ class Build : NukeBuild } }); + Target GenerateReadmes => _ => _ + .Executes(() => + { + DotNet("tool run mdsnippets", RootDirectory / "docs"); + }); + Target PushToNuGet => _ => _ .DependsOn(Pack) .OnlyWhenStatic(() => IsTag && IsServerBuild && IsWin) diff --git a/src/NetEscapades.EnumGenerators.Generators/README.md b/docs/NetEscapades.EnumGenerators.Generators.md similarity index 94% rename from src/NetEscapades.EnumGenerators.Generators/README.md rename to docs/NetEscapades.EnumGenerators.Generators.md index 4d9a56b8..084ebfd4 100644 --- a/src/NetEscapades.EnumGenerators.Generators/README.md +++ b/docs/NetEscapades.EnumGenerators.Generators.md @@ -1,3 +1,10 @@ + + # ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators.Generators ![Build status](https://github.com/andrewlock/NetEscapades.EnumGenerators/actions/workflows/BuildAndPack.yml/badge.svg) @@ -11,7 +18,7 @@ In general, we recommend installing the [NetEscapades.EnumGenerators](https://ww ## Why use these packages? -Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. +Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. For example, the following benchmark shows the advantage of calling `ToStringFast()` over `ToString()`: @@ -44,8 +51,9 @@ public enum Color Blue = 1, } ``` + -The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). For example, imagine you have this code, which uses the `Color` enum defined above: @@ -58,11 +66,11 @@ Console.WriteLine("You chose: " + choice.ToString()); By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: ```csharp -// The compiler replaces the call with this 👇 +// The compiler replaces the call with this 👇 Console.WriteLine("You chose: " + choice.ToStringFast()); ``` -There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. ## Adding NetEscapades.EnumGenerators.Generators to your project @@ -85,14 +93,14 @@ This adds a `` to your project. You can additionally mark the - ``` -Adding the package will automatically add a marker attribute, `[EnumExtensions]`, to your project. +Adding the package will automatically add a marker attribute, `[EnumExtensions]`, to your project. To use the generator, add the `[EnumExtensions]` attribute to an enum. For example: @@ -102,7 +110,7 @@ public enum MyEnum { First, - [Display(Name = "2nd")] + [EnumMember(Value = "2nd")] Second, } ``` @@ -160,7 +168,7 @@ public static partial class MyEnumExtensions return true; } - + return name switch { nameof(MyEnum.First) => true, @@ -181,7 +189,7 @@ public static partial class MyEnumExtensions public static bool TryParse(string? name, out MyEnum value) => TryParse(name, out value, false, false); - public static bool TryParse(string? name, out MyEnum value, bool ignoreCase) + public static bool TryParse(string? name, out MyEnum value, bool ignoreCase) => TryParse(name, out value, ignoreCase, false); public static bool TryParse(string? name, out MyEnum value, bool ignoreCase, bool allowMatchingMetadataAttribute) @@ -331,14 +339,16 @@ You can also set the `IsInternal` property on individual enums using the `[EnumE [EnumExtensions(IsInternal = true)] public enum MyEnum { ... } ``` + or for external enums: ```csharp // Force ExternalEnum's extensions to be internal [EnumExtensions(IsInternal = true)] ``` + -## Usage Analyzers +## Usage Analyzers _NetEscapades.EnumGenerators_ includes optional analyzers that encourage the use of the generated extension methods instead of the built-in `System.Enum` methods. These analyzers can help improve performance by suggesting the faster, generated, alternatives like `ToStringFast()`, `HasFlagFast()`, and `TryParse()`. @@ -363,7 +373,7 @@ After using one of these configuration options, the analyzers in your project sh ### Configuring analyzer severity (optional) -Once enabled, you can optionally configure the severity of individual analyzer rules using one or more [`.editorconfig` files](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#editorconfig). For example, the following changes all the built-in analyzers to report usages as errors instead of warnings +Once enabled, you can optionally configure the severity of individual analyzer rules using one or more [`.editorconfig` files](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#editorconfig). For example, the following changes all the built-in analyzers to report usages as errors instead of warnings ```ini [*.{cs,vb}] @@ -406,9 +416,80 @@ Valid severity values include: `none`, `silent`, `suggestion`, `warning`, and `e All usage analyzers include automatic code fixes. When a diagnostic is triggered, you can use the quick fix functionality in your IDE to automatically replace the `System.Enum` method with the corresponding generated extension method: -![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) +![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) + +## Enabling automatic interception + +Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. + +> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. + +To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: + +```bash +dotnet add package NetEscapades.EnumGenerators.Interceptors +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. + +By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. + +```csharp +[assembly:Interceptable] +[assembly:Interceptable] +``` + +If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. + +```csharp +[EnumExtensions(IsInterceptable = false)] +public enum Colour +{ + Red = 0, + Blue = 1, +} +``` + +Interception only works when the target type is unambiguously an interceptable enum, so it won't work + +- When `ToString()` is called in other source generated code. +- When `ToString()` is called in already-compiled code. +- If the `ToString()` call is _implicit_ (for example in `string` interpolation) +- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` +- If the `ToString()` call is made on a generic type + +For example: + +```csharp +// All the examples in this method CAN be intercepted +public void CanIntercept() +{ + var ok1 = Color.Red.ToString(); // ✅ + var red = Color.Red; + var ok2 = red.ToString(); // ✅ + var ok3 = "The colour is " + red.ToString(); // ✅ + var ok4 = $"The colour is {red.ToString()}"; // ✅ +} + +// The examples in this method can NOT be intercepted +public void CantIntercept() +{ + var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type + var bad2 = ((object)Color.Red).ToString(); // ❌ Base type + + var bad3 = "The colour is " + red; // ❌ implicit + var bad4 = $"The colour is {red}"; // ❌ implicit + + string Write(T val) + where T : Enum + { + return val.ToString(); // ❌ generic + } +} +``` + -## Package referencing options +## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. @@ -460,12 +541,12 @@ namespace NetEscapades.EnumGenerators; public readonly struct EnumParseOptions { } /// -/// Options to apply when calling ToStringFast on an enum. +/// Options to apply when calling ToStringFast on an enum. /// public readonly struct SerializationOptions /// -/// Transform to apply when calling ToStringFast +/// Transform to apply when calling ToStringFast /// public enum SerializationTransform ``` @@ -479,7 +560,7 @@ 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 @@ -538,79 +619,9 @@ The final option is to reference [NetEscapades.EnumGenerators.Generators](https: ``` > [!WARNING] -> When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. - -## Enabling automatic interception - -Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. - -> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. - -To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: - -```bash -dotnet add package NetEscapades.EnumGenerators.Interceptors -``` - -This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. - -By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. - -```csharp -[assembly:Interceptable] -[assembly:Interceptable] -``` - -If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. - -```csharp -[EnumExtensions(IsInterceptable = false)] -public enum Colour -{ - Red = 0, - Blue = 1, -} -``` - -Interception only works when the target type is unambiguously an interceptable enum, so it won't work - -- When `ToString()` is called in other source generated code. -- When `ToString()` is called in already-compiled code. -- If the `ToString()` call is _implicit_ (for example in `string` interpolation) -- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` -- If the `ToString()` call is made on a generic type - -For example: - -```csharp -// All the examples in this method CAN be intercepted -public void CanIntercept() -{ - var ok1 = Color.Red.ToString(); // ✅ - var red = Color.Red; - var ok2 = red.ToString(); // ✅ - var ok3 = "The colour is " + red.ToString(); // ✅ - var ok4 = $"The colour is {red.ToString()}"; // ✅ -} - -// The examples in this method can NOT be intercepted -public void CantIntercept() -{ - var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type - var bad2 = ((object)Color.Red).ToString(); // ❌ Base type - - var bad3 = "The colour is " + red; // ❌ implicit - var bad4 = $"The colour is {red}"; // ❌ implicit - - string Write(T val) - where T : Enum - { - return val.ToString(); // ❌ generic - } -} -``` +> When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. -## Preserving usages of the `[EnumExtensions]` attribute +## Preserving usages of the `[EnumExtensions]` attribute The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. @@ -628,3 +639,4 @@ The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute ``` + diff --git a/docs/NetEscapades.EnumGenerators.Generators.source.md b/docs/NetEscapades.EnumGenerators.Generators.source.md new file mode 100644 index 00000000..90d56a7f --- /dev/null +++ b/docs/NetEscapades.EnumGenerators.Generators.source.md @@ -0,0 +1,68 @@ +# ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators.Generators + +![Build status](https://github.com/andrewlock/NetEscapades.EnumGenerators/actions/workflows/BuildAndPack.yml/badge.svg) +[![NuGet](https://img.shields.io/nuget/v/NetEscapades.EnumGenerators.svg)](https://www.nuget.org/packages/NetEscapades.EnumGenerators/) + +[NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) is a source generator package that generates extension methods for enums, to allow fast "reflection". + +In general, we recommend installing the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage. [See below](#package-referencing-options) for details on choosing between these two packages. + +> [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) requires the .NET 7 SDK or higher. [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) requires the .NET 8.0.400 SDK or higher. You can still target earlier frameworks like .NET Core 3.1 etc, the version requirement only applies to the version of the .NET SDK installed. + +## Why use these packages? + +include: benchmark + +## Adding NetEscapades.EnumGenerators.Generators to your project + +Add the package to your application using + +```bash +dotnet add package NetEscapades.EnumGenerators.Generators +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`. + +> Setting `PrivateAssets="all"` means any projects referencing this one won't get a reference to the _NetEscapades.EnumGenerators.Generators_ package. Setting `ExcludeAssets="runtime"` ensures the _NetEscapades.EnumGenerators.Attributes.dll_ file is not copied to your build output (it is not required at runtime). + +```xml + + + + Exe + net8.0 + + + + + + + +``` + +include: enum-usage + +include: usage-analyzers + +## Enabling automatic interception + +include: interceptor-intro + +Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. + +> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. + +To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: + +```bash +dotnet add package NetEscapades.EnumGenerators.Interceptors +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. + +include: interception-config + +include: package-referencing + +include: preserving-usages diff --git a/src/NetEscapades.EnumGenerators.Interceptors/README.md b/docs/NetEscapades.EnumGenerators.Interceptors.md similarity index 91% rename from src/NetEscapades.EnumGenerators.Interceptors/README.md rename to docs/NetEscapades.EnumGenerators.Interceptors.md index 96254b04..3123d8e4 100644 --- a/src/NetEscapades.EnumGenerators.Interceptors/README.md +++ b/docs/NetEscapades.EnumGenerators.Interceptors.md @@ -1,3 +1,10 @@ + + # ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators.Interceptors A source generator interceptor for automatically intercepting calls to `ToString()` on enums, and replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) @@ -10,7 +17,7 @@ Many methods that operate with enums, such as the `ToString()` or `HasFlag()` me The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). -> Interceptors were introduced as an experimental feature in C#12 with .NET 8, and are stable in .NET 9. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. +> Interceptors were introduced as an experimental feature in C#12 with .NET 8, and are stable in .NET 9. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. For example if you have this code: @@ -28,7 +35,7 @@ public enum Color When you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: ```csharp -// The compiler replaces the call with this 👇 +// The compiler replaces the call with this 👇 Console.WriteLine("You chose: " + choice.ToStringFast()); ``` @@ -63,7 +70,7 @@ This adds a `` to your project. You can additionally mark the ## Enabling interception for an enum -By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. +By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. ```csharp [assembly:Interceptable] @@ -107,7 +114,7 @@ public void CantIntercept() { var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type var bad2 = ((object)Color.Red).ToString(); // ❌ Base type - + var bad3 = "The colour is " + red; // ❌ implicit var bad4 = $"The colour is {red}"; // ❌ implicit @@ -117,4 +124,5 @@ public void CantIntercept() return val.ToString(); // ❌ generic } } -``` \ No newline at end of file +``` + diff --git a/docs/NetEscapades.EnumGenerators.Interceptors.source.md b/docs/NetEscapades.EnumGenerators.Interceptors.source.md new file mode 100644 index 00000000..759b5821 --- /dev/null +++ b/docs/NetEscapades.EnumGenerators.Interceptors.source.md @@ -0,0 +1,66 @@ +# ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators.Interceptors + +A source generator interceptor for automatically intercepting calls to `ToString()` on enums, and replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) + +> This source generator requires the .NET 8.0.400 SDK. You can target earlier frameworks like .NET Core 3.1 etc, but the _SDK_ must be at least 8.0.400 + +## Why use this package? + +Many methods that operate with enums, such as the `ToString()` or `HasFlag()` method, are surprisingly slow. The [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) uses a source generator to provide _fast_ versions of these methods, such as `ToStringFast()` or `HasFlagFast()`. + +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). + +> Interceptors were introduced as an experimental feature in C#12 with .NET 8, and are stable in .NET 9. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. + +For example if you have this code: + +```csharp +var choice = Color.Red; +Console.WriteLine("You chose: " + choice.ToString()); + +public enum Color +{ + Red = 0, + Blue = 1, +} +``` + +When you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: + +```csharp +// The compiler replaces the call with this 👇 +Console.WriteLine("You chose: " + choice.ToStringFast()); +``` + +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. + +> In addition to the caveats listed below, the interceptor approach unfortunately requires boxing the enum, so in general we recommend enabling [the usage analyzers](https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/main/README.md#usage-analyzers) in _NetEscapades.EnumGenerators_ instead of using this package. + +## Adding NetEscapades.EnumGenerators.Interceptors to your project + +Add the package to your application using: + +```bash +dotnet add package NetEscapades.EnumGenerators.Interceptors +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`. + +```xml + + + + Exe + net8.0 + + + + + + + +``` + +## Enabling interception for an enum + +include: interception-config diff --git a/docs/NetEscapades.EnumGenerators.RuntimeDependencies.md b/docs/NetEscapades.EnumGenerators.RuntimeDependencies.md new file mode 100644 index 00000000..bb9d59a0 --- /dev/null +++ b/docs/NetEscapades.EnumGenerators.RuntimeDependencies.md @@ -0,0 +1,13 @@ + + +# ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators.RuntimeDependencies + +![Build status](https://github.com/andrewlock/NetEscapades.EnumGenerators/actions/workflows/BuildAndPack.yml/badge.svg) +[![NuGet](https://img.shields.io/nuget/v/NetEscapades.EnumGenerators.svg)](https://www.nuget.org/packages/NetEscapades.EnumGenerators/) + +[NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies) provides runtime dependencies for enum extension methods generated by the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) and [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) source generators. See those packages for details and to decide whether you need to install this package directly. diff --git a/src/NetEscapades.EnumGenerators.RuntimeDependencies/README.md b/docs/NetEscapades.EnumGenerators.RuntimeDependencies.source.md similarity index 100% rename from src/NetEscapades.EnumGenerators.RuntimeDependencies/README.md rename to docs/NetEscapades.EnumGenerators.RuntimeDependencies.source.md diff --git a/README.md b/docs/NetEscapades.EnumGenerators.md similarity index 79% rename from README.md rename to docs/NetEscapades.EnumGenerators.md index aae986cc..8533510e 100644 --- a/README.md +++ b/docs/NetEscapades.EnumGenerators.md @@ -1,17 +1,26 @@ + + # ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators ![Build status](https://github.com/andrewlock/NetEscapades.EnumGenerators/actions/workflows/BuildAndPack.yml/badge.svg) [![NuGet](https://img.shields.io/nuget/v/NetEscapades.EnumGenerators.svg)](https://www.nuget.org/packages/NetEscapades.EnumGenerators/) -A Source Generator package that generates extension methods for enums, to allow fast "reflection". +- [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a source generator package that generates extension methods for enums, to allow fast "reflection". +- [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) is a source generator interceptor for intercepting calls to `ToString()` on enums, and automatically replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators). + -> This source generator requires the .NET 7 SDK. You can target earlier frameworks like .NET Core 3.1 etc, but the _SDK_ must be at least 7.0.100 +> [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) requires the .NET 7 SDK or higher. [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) is experimental and requires the .NET 8.0.400 SDK or higher. You can still target earlier frameworks like .NET Core 3.1 etc, the version requirement only applies to the version of the .NET SDK installed. [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage, [see below](#package-referencing-options) for details about the associated packages and other options for packages to reference. -## Why use this package? +## Why use these packages? -Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. +Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. For example, the following benchmark shows the advantage of calling `ToStringFast()` over `ToString()`: @@ -24,15 +33,16 @@ Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cor DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT ``` -| 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 | - | - | +| 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 | 0.004 | 1.000 | 24 B | | ToStringFast | `net10.0` | 0.0050 ns | 0.0202 ns | 0.0189 ns | 0.001 | - | - | + Enabling these additional extension methods is as simple as adding an attribute to your enum: ```csharp @@ -43,6 +53,26 @@ public enum Color Blue = 1, } ``` + + +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). + + +For example, imagine you have this code, which uses the `Color` enum defined above: + +```csharp +var choice = Color.Red; +Console.WriteLine("You chose: " + choice.ToString()); +``` + +By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: + +```csharp +// The compiler replaces the call with this 👇 +Console.WriteLine("You chose: " + choice.ToStringFast()); +``` + +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. ## Adding NetEscapades.EnumGenerators to your project @@ -72,8 +102,7 @@ This adds a `` to your project: > [!WARNING] > You should _not_ use `PrivateAssets` when referencing the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) package, as the package has runtime dependencies. If you wish to avoid these runtime dependencies, [see below](#package-referencing-options) for alternative approaches. - -Adding the package will automatically add a marker attribute, `[EnumExtensions]`, to your project. +Adding the package will automatically add a marker attribute, `[EnumExtensions]`, to your project. To use the generator, add the `[EnumExtensions]` attribute to an enum. For example: @@ -141,7 +170,7 @@ public static partial class MyEnumExtensions return true; } - + return name switch { nameof(MyEnum.First) => true, @@ -162,7 +191,7 @@ public static partial class MyEnumExtensions public static bool TryParse(string? name, out MyEnum value) => TryParse(name, out value, false, false); - public static bool TryParse(string? name, out MyEnum value, bool ignoreCase) + public static bool TryParse(string? name, out MyEnum value, bool ignoreCase) => TryParse(name, out value, ignoreCase, false); public static bool TryParse(string? name, out MyEnum value, bool ignoreCase, bool allowMatchingMetadataAttribute) @@ -319,12 +348,12 @@ or for external enums: // Force ExternalEnum's extensions to be internal [EnumExtensions(IsInternal = true)] ``` + -## Usage Analyzers +## Usage Analyzers _NetEscapades.EnumGenerators_ includes optional analyzers that encourage the use of the generated extension methods instead of the built-in `System.Enum` methods. These analyzers can help improve performance by suggesting the faster, generated, alternatives like `ToStringFast()`, `HasFlagFast()`, and `TryParse()`. - ### Enabling the analyzers The usage analyzers are disabled by default. To enable them, set the `EnumGenerator_EnableUsageAnalyzers` MSBuild property to `true` in your project: @@ -389,10 +418,80 @@ Valid severity values include: `none`, `silent`, `suggestion`, `warning`, and `e All usage analyzers include automatic code fixes. When a diagnostic is triggered, you can use the quick fix functionality in your IDE to automatically replace the `System.Enum` method with the corresponding generated extension method: -![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) +![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) + +## Enabling automatic interception + +Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. + +> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. + +To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: + +```bash +dotnet add package NetEscapades.EnumGenerators.Interceptors +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. + +By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. + +```csharp +[assembly:Interceptable] +[assembly:Interceptable] +``` + +If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. + +```csharp +[EnumExtensions(IsInterceptable = false)] +public enum Colour +{ + Red = 0, + Blue = 1, +} +``` + +Interception only works when the target type is unambiguously an interceptable enum, so it won't work + +- When `ToString()` is called in other source generated code. +- When `ToString()` is called in already-compiled code. +- If the `ToString()` call is _implicit_ (for example in `string` interpolation) +- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` +- If the `ToString()` call is made on a generic type + +For example: +```csharp +// All the examples in this method CAN be intercepted +public void CanIntercept() +{ + var ok1 = Color.Red.ToString(); // ✅ + var red = Color.Red; + var ok2 = red.ToString(); // ✅ + var ok3 = "The colour is " + red.ToString(); // ✅ + var ok4 = $"The colour is {red.ToString()}"; // ✅ +} + +// The examples in this method can NOT be intercepted +public void CantIntercept() +{ + var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type + var bad2 = ((object)Color.Red).ToString(); // ❌ Base type -## Package referencing options + var bad3 = "The colour is " + red; // ❌ implicit + var bad4 = $"The colour is {red}"; // ❌ implicit + + string Write(T val) + where T : Enum + { + return val.ToString(); // ❌ generic + } +} +``` + + +## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. @@ -444,12 +543,12 @@ namespace NetEscapades.EnumGenerators; public readonly struct EnumParseOptions { } /// -/// Options to apply when calling ToStringFast on an enum. +/// Options to apply when calling ToStringFast on an enum. /// public readonly struct SerializationOptions /// -/// Transform to apply when calling ToStringFast +/// Transform to apply when calling ToStringFast /// public enum SerializationTransform ``` @@ -463,7 +562,7 @@ 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 @@ -522,96 +621,11 @@ The final option is to reference [NetEscapades.EnumGenerators.Generators](https: ``` > [!WARNING] -> When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. - -## Enabling interception - -Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. - -> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. - -To enable interception for a project, add the interceptor package to your application using: - -```bash -dotnet add package NetEscapades.EnumGenerators.Interceptors -``` - -This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`. - -> Setting `PrivateAssets="all"` means any projects referencing this one won't get a reference to the _NetEscapades.EnumGenerators.Interceptors_ package. Setting `ExcludeAssets="runtime"` ensures the _NetEscapades.EnumGenerators.Interceptors.Attributes.dll_ file is _not_ copied to your build output (it is not required at runtime). - -```xml - - - - Exe - net8.0 - - - - - - - -``` - -By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums defined in the project that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. - -```csharp -[assembly:Interceptable] -[assembly:Interceptable] -``` - -If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. - -```csharp -[EnumExtensions(IsInterceptable = false)] -public enum Colour -{ - Red = 0, - Blue = 1, -} -``` - -Interception only works when the target type is unambiguously an interceptable enum, so it won't work - - -- When `ToString()` is called in other source generated code. -- When `ToString()` is called in already-compiled code. -- If the `ToString()` call is _implicit_ (for example in `string` interpolation) -- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` -- If the `ToString()` call is made on a generic type - -## Embedding the attributes in your project +> When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. -By default, the `[EnumExtensions]` attributes referenced in your application are contained in an external dll. It is also possible to embed the attributes directly in your project, so they appear in the dll when your project is built. If you wish to do this, you must do two things: +## Preserving usages of the `[EnumExtensions]` attribute -1. Define the MSBuild constant `NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES`. This ensures the attributes are embedded in your project -2. Add `compile` to the list of excluded assets in your `` element. This ensures the attributes in your project are referenced, instead of the _NetEscapades.EnumGenerators.Attributes.dll_ library. - -Your project file should look something like this: - -```xml - - - - Exe - net7.0 - - $(DefineConstants);NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES - - - - - -``` - -## Preserving usages of the `[EnumExtensions]` attribute - -The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. - -If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. Note that this means your project will have a runtime-dependency on _NetEscapades.EnumGenerators.Attributes.dll_ so you need to ensure this is included in your build output. +The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. ```xml @@ -625,7 +639,6 @@ If you wish to preserve these attributes in the build output, you can define the - - ``` + diff --git a/docs/NetEscapades.EnumGenerators.source.md b/docs/NetEscapades.EnumGenerators.source.md new file mode 100644 index 00000000..9ec923ca --- /dev/null +++ b/docs/NetEscapades.EnumGenerators.source.md @@ -0,0 +1,70 @@ +# ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators + +![Build status](https://github.com/andrewlock/NetEscapades.EnumGenerators/actions/workflows/BuildAndPack.yml/badge.svg) +[![NuGet](https://img.shields.io/nuget/v/NetEscapades.EnumGenerators.svg)](https://www.nuget.org/packages/NetEscapades.EnumGenerators/) + +- [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a source generator package that generates extension methods for enums, to allow fast "reflection". +- [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) is a source generator interceptor for intercepting calls to `ToString()` on enums, and automatically replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators). + + +> [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) requires the .NET 7 SDK or higher. [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) is experimental and requires the .NET 8.0.400 SDK or higher. You can still target earlier frameworks like .NET Core 3.1 etc, the version requirement only applies to the version of the .NET SDK installed. + +[NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage, [see below](#package-referencing-options) for details about the associated packages and other options for packages to reference. + +## Why use these packages? + +include: benchmark + +## Adding NetEscapades.EnumGenerators to your project + +Add the package to your application using + +```bash +dotnet add package NetEscapades.EnumGenerators +``` + +This adds a `` to your project: + +```xml + + + + Exe + net8.0 + + + + + + + +``` + +> [!WARNING] +> You should _not_ use `PrivateAssets` when referencing the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) package, as the package has runtime dependencies. If you wish to avoid these runtime dependencies, [see below](#package-referencing-options) for alternative approaches. + +include: enum-usage + +include: usage-analyzers + +## Enabling automatic interception + +include: interceptor-intro + +Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. + +> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. + +To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: + +```bash +dotnet add package NetEscapades.EnumGenerators.Interceptors +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. + +include: interception-config + +include: package-referencing + +include: preserving-usages diff --git a/src/NetEscapades.EnumGenerators/README.md b/docs/README.md similarity index 94% rename from src/NetEscapades.EnumGenerators/README.md rename to docs/README.md index 81c4a881..0899276a 100644 --- a/src/NetEscapades.EnumGenerators/README.md +++ b/docs/README.md @@ -1,3 +1,10 @@ + + # ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators ![Build status](https://github.com/andrewlock/NetEscapades.EnumGenerators/actions/workflows/BuildAndPack.yml/badge.svg) @@ -13,7 +20,7 @@ ## Why use these packages? -Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. +Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. For example, the following benchmark shows the advantage of calling `ToStringFast()` over `ToString()`: @@ -46,8 +53,9 @@ public enum Color Blue = 1, } ``` + -The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). For example, imagine you have this code, which uses the `Color` enum defined above: @@ -60,11 +68,11 @@ Console.WriteLine("You chose: " + choice.ToString()); By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: ```csharp -// The compiler replaces the call with this 👇 +// The compiler replaces the call with this 👇 Console.WriteLine("You chose: " + choice.ToStringFast()); ``` -There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. ## Adding NetEscapades.EnumGenerators to your project @@ -94,7 +102,7 @@ This adds a `` to your project: > [!WARNING] > You should _not_ use `PrivateAssets` when referencing the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) package, as the package has runtime dependencies. If you wish to avoid these runtime dependencies, [see below](#package-referencing-options) for alternative approaches. -Adding the package will automatically add a marker attribute, `[EnumExtensions]`, to your project. +Adding the package will automatically add a marker attribute, `[EnumExtensions]`, to your project. To use the generator, add the `[EnumExtensions]` attribute to an enum. For example: @@ -104,7 +112,7 @@ public enum MyEnum { First, - [Display(Name = "2nd")] + [EnumMember(Value = "2nd")] Second, } ``` @@ -162,7 +170,7 @@ public static partial class MyEnumExtensions return true; } - + return name switch { nameof(MyEnum.First) => true, @@ -183,7 +191,7 @@ public static partial class MyEnumExtensions public static bool TryParse(string? name, out MyEnum value) => TryParse(name, out value, false, false); - public static bool TryParse(string? name, out MyEnum value, bool ignoreCase) + public static bool TryParse(string? name, out MyEnum value, bool ignoreCase) => TryParse(name, out value, ignoreCase, false); public static bool TryParse(string? name, out MyEnum value, bool ignoreCase, bool allowMatchingMetadataAttribute) @@ -303,7 +311,7 @@ You can set the default metadata source to use for a whole project by setting th ``` -You can override the name of the extension class by setting `ExtensionClassName` in the attribute and/or the namespace of the class by setting `ExtensionClassNamespace`. +You can override the name of the extension class by setting `ExtensionClassName` in the attribute and/or the namespace of the class by setting `ExtensionClassNamespace`. ### Controlling extension accessibility @@ -340,8 +348,9 @@ or for external enums: // Force ExternalEnum's extensions to be internal [EnumExtensions(IsInternal = true)] ``` + -## Usage Analyzers +## Usage Analyzers _NetEscapades.EnumGenerators_ includes optional analyzers that encourage the use of the generated extension methods instead of the built-in `System.Enum` methods. These analyzers can help improve performance by suggesting the faster, generated, alternatives like `ToStringFast()`, `HasFlagFast()`, and `TryParse()`. @@ -366,7 +375,7 @@ After using one of these configuration options, the analyzers in your project sh ### Configuring analyzer severity (optional) -Once enabled, you can optionally configure the severity of individual analyzer rules using one or more [`.editorconfig` files](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#editorconfig). For example, the following changes all the built-in analyzers to report usages as errors instead of warnings +Once enabled, you can optionally configure the severity of individual analyzer rules using one or more [`.editorconfig` files](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#editorconfig). For example, the following changes all the built-in analyzers to report usages as errors instead of warnings ```ini [*.{cs,vb}] @@ -409,7 +418,7 @@ Valid severity values include: `none`, `silent`, `suggestion`, `warning`, and `e All usage analyzers include automatic code fixes. When a diagnostic is triggered, you can use the quick fix functionality in your IDE to automatically replace the `System.Enum` method with the corresponding generated extension method: -![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) +![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) ## Enabling automatic interception @@ -425,7 +434,7 @@ dotnet add package NetEscapades.EnumGenerators.Interceptors This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. -By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. +By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. ```csharp [assembly:Interceptable] @@ -469,7 +478,7 @@ public void CantIntercept() { var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type var bad2 = ((object)Color.Red).ToString(); // ❌ Base type - + var bad3 = "The colour is " + red; // ❌ implicit var bad4 = $"The colour is {red}"; // ❌ implicit @@ -480,8 +489,9 @@ public void CantIntercept() } } ``` + -## Package referencing options +## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. @@ -503,7 +513,7 @@ The default approach is to reference the meta-package in your project. The runti In some cases you may not want these dependencies to flow to other projects. This is common when you are using _NetEscapades.EnumGenerators_ internally in your own library, for example. In this scenario, we suggest you take the following approach: - Reference [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) directly, and set `PrivateAssets=All` -- _Optionally_ reference [NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies) directly. +- _Optionally_ reference [NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies) directly. ```xml @@ -533,12 +543,12 @@ namespace NetEscapades.EnumGenerators; public readonly struct EnumParseOptions { } /// -/// Options to apply when calling ToStringFast on an enum. +/// Options to apply when calling ToStringFast on an enum. /// public readonly struct SerializationOptions /// -/// Transform to apply when calling ToStringFast +/// Transform to apply when calling ToStringFast /// public enum SerializationTransform ``` @@ -552,7 +562,7 @@ 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 @@ -611,9 +621,9 @@ The final option is to reference [NetEscapades.EnumGenerators.Generators](https: ``` > [!WARNING] -> When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. +> When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. -## Preserving usages of the `[EnumExtensions]` attribute +## Preserving usages of the `[EnumExtensions]` attribute The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. @@ -631,3 +641,4 @@ The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute ``` + diff --git a/docs/README.source.md b/docs/README.source.md new file mode 100644 index 00000000..9ec923ca --- /dev/null +++ b/docs/README.source.md @@ -0,0 +1,70 @@ +# ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators + +![Build status](https://github.com/andrewlock/NetEscapades.EnumGenerators/actions/workflows/BuildAndPack.yml/badge.svg) +[![NuGet](https://img.shields.io/nuget/v/NetEscapades.EnumGenerators.svg)](https://www.nuget.org/packages/NetEscapades.EnumGenerators/) + +- [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a source generator package that generates extension methods for enums, to allow fast "reflection". +- [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) is a source generator interceptor for intercepting calls to `ToString()` on enums, and automatically replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators). + + +> [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) requires the .NET 7 SDK or higher. [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) is experimental and requires the .NET 8.0.400 SDK or higher. You can still target earlier frameworks like .NET Core 3.1 etc, the version requirement only applies to the version of the .NET SDK installed. + +[NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage, [see below](#package-referencing-options) for details about the associated packages and other options for packages to reference. + +## Why use these packages? + +include: benchmark + +## Adding NetEscapades.EnumGenerators to your project + +Add the package to your application using + +```bash +dotnet add package NetEscapades.EnumGenerators +``` + +This adds a `` to your project: + +```xml + + + + Exe + net8.0 + + + + + + + +``` + +> [!WARNING] +> You should _not_ use `PrivateAssets` when referencing the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) package, as the package has runtime dependencies. If you wish to avoid these runtime dependencies, [see below](#package-referencing-options) for alternative approaches. + +include: enum-usage + +include: usage-analyzers + +## Enabling automatic interception + +include: interceptor-intro + +Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. + +> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. + +To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: + +```bash +dotnet add package NetEscapades.EnumGenerators.Interceptors +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. + +include: interception-config + +include: package-referencing + +include: preserving-usages diff --git a/docs/fragments/benchmark.include.md b/docs/fragments/benchmark.include.md new file mode 100644 index 00000000..f34952c6 --- /dev/null +++ b/docs/fragments/benchmark.include.md @@ -0,0 +1,33 @@ +Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. + + +For example, the following benchmark shows the advantage of calling `ToStringFast()` over `ToString()`: + +``` ini +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1348 (20H2/October2020Update) +Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores + DefaultJob : .NET Framework 4.8 (4.8.4420.0), X64 RyuJIT +.NET SDK=6.0.100 + DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT +``` + +| 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 | 0.004 | 1.000 | 24 B | +| ToStringFast | `net10.0` | 0.0050 ns | 0.0202 ns | 0.0189 ns | 0.001 | - | - | + + +Enabling these additional extension methods is as simple as adding an attribute to your enum: + +```csharp +[EnumExtensions] // 👈 Add this +public enum Color +{ + Red = 0, + Blue = 1, +} +``` diff --git a/docs/fragments/enum-usage.include.md b/docs/fragments/enum-usage.include.md new file mode 100644 index 00000000..8b8a5ce2 --- /dev/null +++ b/docs/fragments/enum-usage.include.md @@ -0,0 +1,246 @@ +Adding the package will automatically add a marker attribute, `[EnumExtensions]`, to your project. + +To use the generator, add the `[EnumExtensions]` attribute to an enum. For example: + +```csharp +[EnumExtensions] +public enum MyEnum +{ + First, + + [EnumMember(Value = "2nd")] + Second, +} +``` + +This will generate a class called `MyEnumExtensions` (by default), which contains a number of helper methods. For example: + +```csharp +public static partial class MyEnumExtensions +{ + public const int Length = 2; + + public static string ToStringFast(this MyEnum value, bool useMetadataAttributes) + => useMetadataAttributes ? value.ToStringFastWithMetadata() : value.ToStringFast(); + + public static string ToStringFast(this MyEnum value) + => value switch + { + MyEnum.First => nameof(MyEnum.First), + MyEnum.Second => nameof(MyEnum.Second), + _ => value.ToString(), + }; + + private static string ToStringFastWithMetadata(this MyEnum value) + => value switch + { + MyEnum.First => nameof(MyEnum.First), + MyEnum.Second => "2nd", + _ => value.ToString(), + }; + + public static bool IsDefined(MyEnum value) + => value switch + { + MyEnum.First => true, + MyEnum.Second => true, + _ => false, + }; + + public static bool IsDefined(string name) => IsDefined(name, allowMatchingMetadataAttribute: false); + + public static bool IsDefined(string name, bool allowMatchingMetadataAttribute) + { + var isDefinedInDisplayAttribute = false; + if (allowMatchingMetadataAttribute) + { + isDefinedInDisplayAttribute = name switch + { + "2nd" => true, + _ => false, + }; + } + + if (isDefinedInDisplayAttribute) + { + return true; + } + + + return name switch + { + nameof(MyEnum.First) => true, + nameof(MyEnum.Second) => true, + _ => false, + }; + } + + public static MyEnum Parse(string? name) + => TryParse(name, out var value, false, false) ? value : ThrowValueNotFound(name); + + public static MyEnum Parse(string? name, bool ignoreCase) + => TryParse(name, out var value, ignoreCase, false) ? value : ThrowValueNotFound(name); + + public static MyEnum Parse(string? name, bool ignoreCase, bool allowMatchingMetadataAttribute) + => TryParse(name, out var value, ignoreCase, allowMatchingMetadataAttribute) ? value : throw new ArgumentException($"Requested value '{name}' was not found."); + + public static bool TryParse(string? name, out MyEnum value) + => TryParse(name, out value, false, false); + + public static bool TryParse(string? name, out MyEnum value, bool ignoreCase) + => TryParse(name, out value, ignoreCase, false); + + public static bool TryParse(string? name, out MyEnum value, bool ignoreCase, bool allowMatchingMetadataAttribute) + => ignoreCase + ? TryParseIgnoreCase(name, out value, allowMatchingMetadataAttribute) + : TryParseWithCase(name, out value, allowMatchingMetadataAttribute); + + private static bool TryParseIgnoreCase(string? name, out MyEnum value, bool allowMatchingMetadataAttribute) + { + if (allowMatchingMetadataAttribute) + { + switch (name) + { + case string s when s.Equals("2nd", System.StringComparison.OrdinalIgnoreCase): + value = MyEnum.Second; + return true; + default: + break; + }; + } + + switch (name) + { + case string s when s.Equals(nameof(MyEnum.First), System.StringComparison.OrdinalIgnoreCase): + value = MyEnum.First; + return true; + case string s when s.Equals(nameof(MyEnum.Second), System.StringComparison.OrdinalIgnoreCase): + value = MyEnum.Second; + return true; + case string s when int.TryParse(name, out var val): + value = (MyEnum)val; + return true; + default: + value = default; + return false; + } + } + + private static bool TryParseWithCase(string? name, out MyEnum value, bool allowMatchingMetadataAttribute) + { + if (allowMatchingMetadataAttribute) + { + switch (name) + { + case "2nd": + value = MyEnum.Second; + return true; + default: + break; + }; + } + + switch (name) + { + case nameof(MyEnum.First): + value = MyEnum.First; + return true; + case nameof(MyEnum.Second): + value = MyEnum.Second; + return true; + case string s when int.TryParse(name, out var val): + value = (MyEnum)val; + return true; + default: + value = default; + return false; + } + } + + public static MyEnum[] GetValues() + { + return new[] + { + MyEnum.First, + MyEnum.Second, + }; + } + + public static string[] GetNames() + { + return new[] + { + nameof(MyEnum.First), + nameof(MyEnum.Second), + }; + } +} +``` + +If you create a "Flags" `enum` by decorating it with the `[Flags]` attribute, an additional method is created, which provides a bitwise alternative to the `Enum.HasFlag(flag)` method: + +```csharp +public static bool HasFlagFast(this MyEnum value, MyEnum flag) + => flag == 0 ? true : (value & flag) == flag; +``` + +Note that if you provide a `[EnumMember]` attribute, the value you provide for this attribute can be used by methods like `ToStringFast()` and `TryParse()` by passing the argument `useMetadataAttributes: true`. Alternatively, you can use the `[Display]` or `[Description]` attributes, and set the `MetadataSource` property on the `[EnumExtensions]` attribute e.g. + +```csharp +[EnumExtensions(MetadataSource = MetadataSource.DisplayAttribute)] +public enum EnumWithDisplayNameInNamespace +{ + First = 0, + [Display(Name = "2nd")] + Second = 1, + Third = 2, +} +``` + +Alternatively, you can use `MetadataSource.None` to choose none of the metadata attributes. In this case, the overloads that take a `useMetadataAttributes` parameter will not be emitted. + +You can set the default metadata source to use for a whole project by setting the `EnumGenerator_EnumMetadataSource` property in your project: + +```xml + + EnumMemberAttribute + +``` + +You can override the name of the extension class by setting `ExtensionClassName` in the attribute and/or the namespace of the class by setting `ExtensionClassNamespace`. + +### Controlling extension accessibility + +By default, the generated extension class is `public` if the enum is `public`, and `internal` if the enum is `internal`. You can control the accessibility of the generated extension classes in two ways: + +**Global Configuration (MSBuild Property):** + +Set the `EnumGenerator_ForceInternal` property in your project file to force all generated extension classes to be `internal`: + +```xml + + + true + +``` + +Valid values are: +- `false` (default): Generated extensions follow the enum's accessibility (`public` for `public` enums, `internal` for `internal` enums) +- `true`: Forces all generated extensions to be `internal`, even for `public` enums + +**Per-Enum Configuration:** + +You can also set the `IsInternal` property on individual enums using the `[EnumExtensions]` attribute: + +```csharp +// Force this enum's extensions to be internal +[EnumExtensions(IsInternal = true)] +public enum MyEnum { ... } +``` + +or for external enums: + +```csharp +// Force ExternalEnum's extensions to be internal +[EnumExtensions(IsInternal = true)] +``` diff --git a/docs/fragments/interception-config.include.md b/docs/fragments/interception-config.include.md new file mode 100644 index 00000000..153202ed --- /dev/null +++ b/docs/fragments/interception-config.include.md @@ -0,0 +1,55 @@ +By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. + +```csharp +[assembly:Interceptable] +[assembly:Interceptable] +``` + +If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. + +```csharp +[EnumExtensions(IsInterceptable = false)] +public enum Colour +{ + Red = 0, + Blue = 1, +} +``` + +Interception only works when the target type is unambiguously an interceptable enum, so it won't work + +- When `ToString()` is called in other source generated code. +- When `ToString()` is called in already-compiled code. +- If the `ToString()` call is _implicit_ (for example in `string` interpolation) +- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` +- If the `ToString()` call is made on a generic type + +For example: + +```csharp +// All the examples in this method CAN be intercepted +public void CanIntercept() +{ + var ok1 = Color.Red.ToString(); // ✅ + var red = Color.Red; + var ok2 = red.ToString(); // ✅ + var ok3 = "The colour is " + red.ToString(); // ✅ + var ok4 = $"The colour is {red.ToString()}"; // ✅ +} + +// The examples in this method can NOT be intercepted +public void CantIntercept() +{ + var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type + var bad2 = ((object)Color.Red).ToString(); // ❌ Base type + + var bad3 = "The colour is " + red; // ❌ implicit + var bad4 = $"The colour is {red}"; // ❌ implicit + + string Write(T val) + where T : Enum + { + return val.ToString(); // ❌ generic + } +} +``` diff --git a/docs/fragments/interceptor-intro.include.md b/docs/fragments/interceptor-intro.include.md new file mode 100644 index 00000000..d65bdfd0 --- /dev/null +++ b/docs/fragments/interceptor-intro.include.md @@ -0,0 +1,18 @@ +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). + + +For example, imagine you have this code, which uses the `Color` enum defined above: + +```csharp +var choice = Color.Red; +Console.WriteLine("You chose: " + choice.ToString()); +``` + +By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: + +```csharp +// The compiler replaces the call with this 👇 +Console.WriteLine("You chose: " + choice.ToStringFast()); +``` + +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. diff --git a/docs/fragments/package-referencing.include.md b/docs/fragments/package-referencing.include.md new file mode 100644 index 00000000..abe515e4 --- /dev/null +++ b/docs/fragments/package-referencing.include.md @@ -0,0 +1,131 @@ +## Package referencing options + +[NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. + +``` +NetEscapades.EnumGenerators + |____NetEscapades.EnumGenerators.Generators + |____NetEscapades.EnumGenerators.RuntimeDependencies +``` + +These packages provide the following functionality: +- `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. + +### Avoiding runtime dependencies + +In some cases you may not want these dependencies to flow to other projects. This is common when you are using _NetEscapades.EnumGenerators_ internally in your own library, for example. In this scenario, we suggest you take the following approach: + +- Reference [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) directly, and set `PrivateAssets=All` +- _Optionally_ reference [NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies) directly. + + +```xml + + + + Exe + net8.0 + + + + + + + + +``` + +The [NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies) packages is a "normal" dependency, that contains types that are used by the generated code, such as `EnumParseOptions`, `SerializationOptions`, and `SerializationTransform`: + +```csharp +namespace NetEscapades.EnumGenerators; + +/// +/// Defines the options use when parsing enums using members provided by NetEscapades.EnumGenerator. +/// +public readonly struct EnumParseOptions { } + +/// +/// Options to apply when calling ToStringFast on an enum. +/// +public readonly struct SerializationOptions + +/// +/// Transform to apply when calling ToStringFast +/// +public enum SerializationTransform +``` + +If the [NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies) package is not found, the generated code creates nested versions of the dependencies in each generated extension method instead: + +```csharp +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 has both upsides and downsides: + +- It avoids placing downstream dependency requirements on consumers of your library. +- If the generated extension methods are `internal`, the generated runtime dependencies are also `internal`, and so not exposed to downstream consumers. +- It makes consuming the APIs that use the runtime dependencies more verbose. + + +### Choosing the correct packages for your scenario + +In general, for simplicity, we recommend referencing [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators), and thereby using [NetEscapades.EnumGenerators.RuntimeDependencies](https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies). 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. + +```xml + + + Exe + net8.0 + + + + +``` + +In contrast, if you are producing a reusable library and don't want any runtime dependencies to be exposed to consumers, we recommend using [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) and setting `PrivateAssets=All` and `ExcludeAssets="runtime"`. + +```xml + + + Exe + net8.0 + + + + +``` + +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. + + +```xml + + + Exe + net8.0 + + + + + +``` + +> [!WARNING] +> When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. diff --git a/docs/fragments/preserving-usages.include.md b/docs/fragments/preserving-usages.include.md new file mode 100644 index 00000000..6ff2bf55 --- /dev/null +++ b/docs/fragments/preserving-usages.include.md @@ -0,0 +1,18 @@ +## Preserving usages of the `[EnumExtensions]` attribute + +The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. + +```xml + + + + Exe + net8.0 + + $(DefineConstants);NETESCAPADES_ENUMGENERATORS_USAGES + + + + + +``` diff --git a/docs/fragments/usage-analyzers.include.md b/docs/fragments/usage-analyzers.include.md new file mode 100644 index 00000000..1496a429 --- /dev/null +++ b/docs/fragments/usage-analyzers.include.md @@ -0,0 +1,69 @@ +## Usage Analyzers + +_NetEscapades.EnumGenerators_ includes optional analyzers that encourage the use of the generated extension methods instead of the built-in `System.Enum` methods. These analyzers can help improve performance by suggesting the faster, generated, alternatives like `ToStringFast()`, `HasFlagFast()`, and `TryParse()`. + +### Enabling the analyzers + +The usage analyzers are disabled by default. To enable them, set the `EnumGenerator_EnableUsageAnalyzers` MSBuild property to `true` in your project: + +```xml + + true + +``` + +Alternatively, [add a `.globalconfig` file](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#global-analyzerconfig) to your project with the following content: + +```ini +is_global = true +build_property.EnumGenerator_EnableUsageAnalyzers = true +``` + +After using one of these configuration options, the analyzers in your project should be enabled with the default severity of `Warning`. + +### Configuring analyzer severity (optional) + +Once enabled, you can optionally configure the severity of individual analyzer rules using one or more [`.editorconfig` files](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#editorconfig). For example, the following changes all the built-in analyzers to report usages as errors instead of warnings + +```ini +[*.{cs,vb}] + +# NEEG004: Use ToStringFast() instead of ToString() +dotnet_diagnostic.NEEG004.severity = error + +# NEEG005: Use HasFlagFast() instead of HasFlag() +dotnet_diagnostic.NEEG005.severity = error + +# NEEG006: Use generated IsDefined() instead of Enum.IsDefined() +dotnet_diagnostic.NEEG006.severity = error + +# NEEG007: Use generated Parse() instead of Enum.Parse() +dotnet_diagnostic.NEEG007.severity = error + +# NEEG008: Use generated GetNames() instead of Enum.GetNames() +dotnet_diagnostic.NEEG008.severity = error + +# NEEG009: Use generated GetValues() instead of Enum.GetValues() +dotnet_diagnostic.NEEG009.severity = error + +# NEEG010: Use generated GetValuesAsUnderlyingType() instead of Enum.GetValuesAsUnderlyingType() +dotnet_diagnostic.NEEG010.severity = error + +# NEEG011: Use generated TryParse() instead of Enum.TryParse() +dotnet_diagnostic.NEEG011.severity = error + +# NEEG012: Call ToStringFast() on enum in StringBuilder.Append() for better performance +dotnet_diagnostic.NEEG012.severity = error +``` + +These are reported in both your IDE and via the CLI as Roslyn errors: + +![Demonstrating the Roslyn errors shown for using the non-generated methods](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//analyzer.png) + +Valid severity values include: `none`, `silent`, `suggestion`, `warning`, and `error`. + +### Code fixes + +All usage analyzers include automatic code fixes. When a diagnostic is triggered, you can use the quick fix functionality in your IDE to automatically replace the `System.Enum` method with the corresponding generated extension method: + +![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) diff --git a/docs/mdsnippets.json b/docs/mdsnippets.json new file mode 100644 index 00000000..80e57355 --- /dev/null +++ b/docs/mdsnippets.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/28.0.1/schema.json", + "WriteHeader": true, + "ReadOnly": false, + "LinkFormat": "None", + "OmitSnippetLinks": true, + "ExcludeSnippetDirectories": [] +} diff --git a/src/NetEscapades.EnumGenerators.Generators/NetEscapades.EnumGenerators.Generators.csproj b/src/NetEscapades.EnumGenerators.Generators/NetEscapades.EnumGenerators.Generators.csproj index db03acae..7f691182 100644 --- a/src/NetEscapades.EnumGenerators.Generators/NetEscapades.EnumGenerators.Generators.csproj +++ b/src/NetEscapades.EnumGenerators.Generators/NetEscapades.EnumGenerators.Generators.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.csproj b/src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.csproj index e66a3d55..7bde1e6c 100644 --- a/src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.csproj +++ b/src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/NetEscapades.EnumGenerators.RuntimeDependencies/NetEscapades.EnumGenerators.RuntimeDependencies.csproj b/src/NetEscapades.EnumGenerators.RuntimeDependencies/NetEscapades.EnumGenerators.RuntimeDependencies.csproj index 6b195a82..a819875e 100644 --- a/src/NetEscapades.EnumGenerators.RuntimeDependencies/NetEscapades.EnumGenerators.RuntimeDependencies.csproj +++ b/src/NetEscapades.EnumGenerators.RuntimeDependencies/NetEscapades.EnumGenerators.RuntimeDependencies.csproj @@ -16,7 +16,7 @@ - + \ No newline at end of file diff --git a/src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj b/src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj index eb18cac1..4af0706b 100644 --- a/src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj +++ b/src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj @@ -20,7 +20,7 @@ - + \ No newline at end of file From 0d3cf8f547b34e996ab3647425786aa8e9319c57 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 6 Mar 2026 21:48:29 +0000 Subject: [PATCH 3/7] Run README generation --- .github/workflows/BuildAndPack.yml | 2 +- build/Build.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/BuildAndPack.yml b/.github/workflows/BuildAndPack.yml index 979460e3..7bacae69 100644 --- a/.github/workflows/BuildAndPack.yml +++ b/.github/workflows/BuildAndPack.yml @@ -62,7 +62,7 @@ jobs: user: ${{ secrets.NUGET_USER || 'NOT_SET' }} - name: Run './build.cmd Clean Test TestPackage PushToNuGet - run: ./build.cmd Clean Test TestPackage PushToNuGet + run: ./build.cmd Clean GenerateReadmes Test TestPackage PushToNuGet env: NuGetToken: ${{ steps.login.outputs.NUGET_API_KEY || 'NOT_SET'}} diff --git a/build/Build.cs b/build/Build.cs index 77a97f87..72e73943 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -195,6 +195,7 @@ class Build : NukeBuild }); Target GenerateReadmes => _ => _ + .Before(Pack) .Executes(() => { DotNet("tool run mdsnippets", RootDirectory / "docs"); From 6f0e2f4d53e54dfb0dece63876242db793b59e16 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 6 Mar 2026 21:53:17 +0000 Subject: [PATCH 4/7] Add ToC --- build/Build.cs | 1 + .../NetEscapades.EnumGenerators.Generators.md | 51 +++++++++++------- ...apades.EnumGenerators.Generators.source.md | 4 +- ...etEscapades.EnumGenerators.Interceptors.md | 7 +++ ...ades.EnumGenerators.Interceptors.source.md | 4 +- docs/NetEscapades.EnumGenerators.md | 53 ++++++++++++------- docs/NetEscapades.EnumGenerators.source.md | 2 + docs/README.md | 51 +++++++++++------- docs/README.source.md | 2 + 9 files changed, 115 insertions(+), 60 deletions(-) diff --git a/build/Build.cs b/build/Build.cs index 72e73943..7a06a8cc 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -198,6 +198,7 @@ class Build : NukeBuild .Before(Pack) .Executes(() => { + DotNetToolRestore(); DotNet("tool run mdsnippets", RootDirectory / "docs"); }); diff --git a/docs/NetEscapades.EnumGenerators.Generators.md b/docs/NetEscapades.EnumGenerators.Generators.md index 084ebfd4..d69acdd6 100644 --- a/docs/NetEscapades.EnumGenerators.Generators.md +++ b/docs/NetEscapades.EnumGenerators.Generators.md @@ -16,6 +16,19 @@ In general, we recommend installing the [NetEscapades.EnumGenerators](https://ww > [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) requires the .NET 7 SDK or higher. [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) requires the .NET 8.0.400 SDK or higher. You can still target earlier frameworks like .NET Core 3.1 etc, the version requirement only applies to the version of the .NET SDK installed. + +## Contents + + * [Why use these packages?](#why-use-these-packages) + * [Adding NetEscapades.EnumGenerators.Generators to your project](#adding-netescapadesenumgeneratorsgenerators-to-your-project) + * [Controlling extension accessibility](#controlling-extension-accessibility) + * [Enabling the analyzers](#enabling-the-analyzers) + * [Configuring analyzer severity (optional)](#configuring-analyzer-severity-optional) + * [Code fixes](#code-fixes) + * [Enabling automatic interception](#enabling-automatic-interception) + * [Avoiding runtime dependencies](#avoiding-runtime-dependencies) + * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) + ## Why use these packages? Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. @@ -53,25 +66,6 @@ public enum Color ``` -The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). - - -For example, imagine you have this code, which uses the `Color` enum defined above: - -```csharp -var choice = Color.Red; -Console.WriteLine("You chose: " + choice.ToString()); -``` - -By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: - -```csharp -// The compiler replaces the call with this 👇 -Console.WriteLine("You chose: " + choice.ToStringFast()); -``` - -There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. - ## Adding NetEscapades.EnumGenerators.Generators to your project Add the package to your application using @@ -420,6 +414,25 @@ All usage analyzers include automatic code fixes. When a diagnostic is triggered ## Enabling automatic interception +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). + + +For example, imagine you have this code, which uses the `Color` enum defined above: + +```csharp +var choice = Color.Red; +Console.WriteLine("You chose: " + choice.ToString()); +``` + +By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: + +```csharp +// The compiler replaces the call with this 👇 +Console.WriteLine("You chose: " + choice.ToStringFast()); +``` + +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. + Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. > To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. diff --git a/docs/NetEscapades.EnumGenerators.Generators.source.md b/docs/NetEscapades.EnumGenerators.Generators.source.md index 90d56a7f..d1ec2d99 100644 --- a/docs/NetEscapades.EnumGenerators.Generators.source.md +++ b/docs/NetEscapades.EnumGenerators.Generators.source.md @@ -9,6 +9,8 @@ In general, we recommend installing the [NetEscapades.EnumGenerators](https://ww > [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) requires the .NET 7 SDK or higher. [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) requires the .NET 8.0.400 SDK or higher. You can still target earlier frameworks like .NET Core 3.1 etc, the version requirement only applies to the version of the .NET SDK installed. +toc + ## Why use these packages? include: benchmark @@ -45,7 +47,7 @@ include: enum-usage include: usage-analyzers -## Enabling automatic interception +## Enabling automatic interception (experimental) include: interceptor-intro diff --git a/docs/NetEscapades.EnumGenerators.Interceptors.md b/docs/NetEscapades.EnumGenerators.Interceptors.md index 3123d8e4..1de39910 100644 --- a/docs/NetEscapades.EnumGenerators.Interceptors.md +++ b/docs/NetEscapades.EnumGenerators.Interceptors.md @@ -11,6 +11,13 @@ A source generator interceptor for automatically intercepting calls to `ToString > This source generator requires the .NET 8.0.400 SDK. You can target earlier frameworks like .NET Core 3.1 etc, but the _SDK_ must be at least 8.0.400 + +## Contents + + * [Why use this package?](#why-use-this-package) + * [Adding NetEscapades.EnumGenerators.Interceptors to your project](#adding-netescapadesenumgeneratorsinterceptors-to-your-project) + * [Enabling interception for an enum](#enabling-interception-for-an-enum) + ## Why use this package? Many methods that operate with enums, such as the `ToString()` or `HasFlag()` method, are surprisingly slow. The [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) uses a source generator to provide _fast_ versions of these methods, such as `ToStringFast()` or `HasFlagFast()`. diff --git a/docs/NetEscapades.EnumGenerators.Interceptors.source.md b/docs/NetEscapades.EnumGenerators.Interceptors.source.md index 759b5821..9d09f1e3 100644 --- a/docs/NetEscapades.EnumGenerators.Interceptors.source.md +++ b/docs/NetEscapades.EnumGenerators.Interceptors.source.md @@ -1,9 +1,11 @@ # ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators.Interceptors -A source generator interceptor for automatically intercepting calls to `ToString()` on enums, and replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) +An experimental source generator interceptor for automatically intercepting calls to `ToString()` on enums, and replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) > This source generator requires the .NET 8.0.400 SDK. You can target earlier frameworks like .NET Core 3.1 etc, but the _SDK_ must be at least 8.0.400 +toc + ## Why use this package? Many methods that operate with enums, such as the `ToString()` or `HasFlag()` method, are surprisingly slow. The [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) uses a source generator to provide _fast_ versions of these methods, such as `ToStringFast()` or `HasFlagFast()`. diff --git a/docs/NetEscapades.EnumGenerators.md b/docs/NetEscapades.EnumGenerators.md index 8533510e..759f1547 100644 --- a/docs/NetEscapades.EnumGenerators.md +++ b/docs/NetEscapades.EnumGenerators.md @@ -18,6 +18,19 @@ To change this file edit the source file and then run MarkdownSnippets. [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage, [see below](#package-referencing-options) for details about the associated packages and other options for packages to reference. + +## Contents + + * [Why use these packages?](#why-use-these-packages) + * [Adding NetEscapades.EnumGenerators to your project](#adding-netescapadesenumgenerators-to-your-project) + * [Controlling extension accessibility](#controlling-extension-accessibility) + * [Enabling the analyzers](#enabling-the-analyzers) + * [Configuring analyzer severity (optional)](#configuring-analyzer-severity-optional) + * [Code fixes](#code-fixes) + * [Enabling automatic interception](#enabling-automatic-interception) + * [Avoiding runtime dependencies](#avoiding-runtime-dependencies) + * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) + ## Why use these packages? Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. @@ -55,25 +68,6 @@ public enum Color ``` -The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). - - -For example, imagine you have this code, which uses the `Color` enum defined above: - -```csharp -var choice = Color.Red; -Console.WriteLine("You chose: " + choice.ToString()); -``` - -By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: - -```csharp -// The compiler replaces the call with this 👇 -Console.WriteLine("You chose: " + choice.ToStringFast()); -``` - -There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. - ## Adding NetEscapades.EnumGenerators to your project Add the package to your application using @@ -420,7 +414,26 @@ All usage analyzers include automatic code fixes. When a diagnostic is triggered ![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) -## Enabling automatic interception +## Enabling automatic interception (experimental) + +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). + + +For example, imagine you have this code, which uses the `Color` enum defined above: + +```csharp +var choice = Color.Red; +Console.WriteLine("You chose: " + choice.ToString()); +``` + +By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: + +```csharp +// The compiler replaces the call with this 👇 +Console.WriteLine("You chose: " + choice.ToStringFast()); +``` + +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. diff --git a/docs/NetEscapades.EnumGenerators.source.md b/docs/NetEscapades.EnumGenerators.source.md index 9ec923ca..79332d81 100644 --- a/docs/NetEscapades.EnumGenerators.source.md +++ b/docs/NetEscapades.EnumGenerators.source.md @@ -11,6 +11,8 @@ [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage, [see below](#package-referencing-options) for details about the associated packages and other options for packages to reference. +toc + ## Why use these packages? include: benchmark diff --git a/docs/README.md b/docs/README.md index 0899276a..457a94e7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,19 @@ To change this file edit the source file and then run MarkdownSnippets. [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage, [see below](#package-referencing-options) for details about the associated packages and other options for packages to reference. + +## Contents + + * [Why use these packages?](#why-use-these-packages) + * [Adding NetEscapades.EnumGenerators to your project](#adding-netescapadesenumgenerators-to-your-project) + * [Controlling extension accessibility](#controlling-extension-accessibility) + * [Enabling the analyzers](#enabling-the-analyzers) + * [Configuring analyzer severity (optional)](#configuring-analyzer-severity-optional) + * [Code fixes](#code-fixes) + * [Enabling automatic interception](#enabling-automatic-interception) + * [Avoiding runtime dependencies](#avoiding-runtime-dependencies) + * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) + ## Why use these packages? Many methods that work with enums are surprisingly slow. Calling `ToString()` or `HasFlag()` on an enum seems like it _should_ be fast, but it often isn't. This package provides a set of extension methods, such as `ToStringFast()` or `HasFlagFast()` that are designed to be very fast, with fewer allocations. @@ -55,25 +68,6 @@ public enum Color ``` -The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). - - -For example, imagine you have this code, which uses the `Color` enum defined above: - -```csharp -var choice = Color.Red; -Console.WriteLine("You chose: " + choice.ToString()); -``` - -By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: - -```csharp -// The compiler replaces the call with this 👇 -Console.WriteLine("You chose: " + choice.ToStringFast()); -``` - -There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. - ## Adding NetEscapades.EnumGenerators to your project Add the package to your application using @@ -422,6 +416,25 @@ All usage analyzers include automatic code fixes. When a diagnostic is triggered ## Enabling automatic interception +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). + + +For example, imagine you have this code, which uses the `Color` enum defined above: + +```csharp +var choice = Color.Red; +Console.WriteLine("You chose: " + choice.ToString()); +``` + +By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: + +```csharp +// The compiler replaces the call with this 👇 +Console.WriteLine("You chose: " + choice.ToStringFast()); +``` + +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. + Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. > To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. diff --git a/docs/README.source.md b/docs/README.source.md index 9ec923ca..79332d81 100644 --- a/docs/README.source.md +++ b/docs/README.source.md @@ -11,6 +11,8 @@ [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage, [see below](#package-referencing-options) for details about the associated packages and other options for packages to reference. +toc + ## Why use these packages? include: benchmark From 1c79be7c016ac267a6426527a094e62a59ab4b7f Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 6 Mar 2026 22:11:18 +0000 Subject: [PATCH 5/7] Reorder sections --- .../NetEscapades.EnumGenerators.Generators.md | 184 +++++++++--------- ...apades.EnumGenerators.Generators.source.md | 4 +- ...etEscapades.EnumGenerators.Interceptors.md | 2 +- docs/NetEscapades.EnumGenerators.md | 184 +++++++++--------- docs/NetEscapades.EnumGenerators.source.md | 6 +- docs/README.md | 184 +++++++++--------- docs/README.source.md | 6 +- 7 files changed, 285 insertions(+), 285 deletions(-) diff --git a/docs/NetEscapades.EnumGenerators.Generators.md b/docs/NetEscapades.EnumGenerators.Generators.md index d69acdd6..45aa04b9 100644 --- a/docs/NetEscapades.EnumGenerators.Generators.md +++ b/docs/NetEscapades.EnumGenerators.Generators.md @@ -25,9 +25,9 @@ In general, we recommend installing the [NetEscapades.EnumGenerators](https://ww * [Enabling the analyzers](#enabling-the-analyzers) * [Configuring analyzer severity (optional)](#configuring-analyzer-severity-optional) * [Code fixes](#code-fixes) - * [Enabling automatic interception](#enabling-automatic-interception) * [Avoiding runtime dependencies](#avoiding-runtime-dependencies) - * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) + * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) + * [Enabling automatic interception (experimental)](#enabling-automatic-interception-experimental) ## Why use these packages? @@ -412,96 +412,6 @@ All usage analyzers include automatic code fixes. When a diagnostic is triggered ![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) -## Enabling automatic interception - -The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). - - -For example, imagine you have this code, which uses the `Color` enum defined above: - -```csharp -var choice = Color.Red; -Console.WriteLine("You chose: " + choice.ToString()); -``` - -By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: - -```csharp -// The compiler replaces the call with this 👇 -Console.WriteLine("You chose: " + choice.ToStringFast()); -``` - -There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. - -Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. - -> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. - -To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: - -```bash -dotnet add package NetEscapades.EnumGenerators.Interceptors -``` - -This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. - -By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. - -```csharp -[assembly:Interceptable] -[assembly:Interceptable] -``` - -If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. - -```csharp -[EnumExtensions(IsInterceptable = false)] -public enum Colour -{ - Red = 0, - Blue = 1, -} -``` - -Interception only works when the target type is unambiguously an interceptable enum, so it won't work - -- When `ToString()` is called in other source generated code. -- When `ToString()` is called in already-compiled code. -- If the `ToString()` call is _implicit_ (for example in `string` interpolation) -- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` -- If the `ToString()` call is made on a generic type - -For example: - -```csharp -// All the examples in this method CAN be intercepted -public void CanIntercept() -{ - var ok1 = Color.Red.ToString(); // ✅ - var red = Color.Red; - var ok2 = red.ToString(); // ✅ - var ok3 = "The colour is " + red.ToString(); // ✅ - var ok4 = $"The colour is {red.ToString()}"; // ✅ -} - -// The examples in this method can NOT be intercepted -public void CantIntercept() -{ - var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type - var bad2 = ((object)Color.Red).ToString(); // ❌ Base type - - var bad3 = "The colour is " + red; // ❌ implicit - var bad4 = $"The colour is {red}"; // ❌ implicit - - string Write(T val) - where T : Enum - { - return val.ToString(); // ❌ generic - } -} -``` - - ## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. @@ -634,6 +544,96 @@ The final option is to reference [NetEscapades.EnumGenerators.Generators](https: > [!WARNING] > When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. +## Enabling automatic interception (experimental) + +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). + + +For example, imagine you have this code, which uses the `Color` enum defined above: + +```csharp +var choice = Color.Red; +Console.WriteLine("You chose: " + choice.ToString()); +``` + +By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: + +```csharp +// The compiler replaces the call with this 👇 +Console.WriteLine("You chose: " + choice.ToStringFast()); +``` + +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. + +Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. + +> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. + +To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: + +```bash +dotnet add package NetEscapades.EnumGenerators.Interceptors +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. + +By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. + +```csharp +[assembly:Interceptable] +[assembly:Interceptable] +``` + +If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. + +```csharp +[EnumExtensions(IsInterceptable = false)] +public enum Colour +{ + Red = 0, + Blue = 1, +} +``` + +Interception only works when the target type is unambiguously an interceptable enum, so it won't work + +- When `ToString()` is called in other source generated code. +- When `ToString()` is called in already-compiled code. +- If the `ToString()` call is _implicit_ (for example in `string` interpolation) +- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` +- If the `ToString()` call is made on a generic type + +For example: + +```csharp +// All the examples in this method CAN be intercepted +public void CanIntercept() +{ + var ok1 = Color.Red.ToString(); // ✅ + var red = Color.Red; + var ok2 = red.ToString(); // ✅ + var ok3 = "The colour is " + red.ToString(); // ✅ + var ok4 = $"The colour is {red.ToString()}"; // ✅ +} + +// The examples in this method can NOT be intercepted +public void CantIntercept() +{ + var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type + var bad2 = ((object)Color.Red).ToString(); // ❌ Base type + + var bad3 = "The colour is " + red; // ❌ implicit + var bad4 = $"The colour is {red}"; // ❌ implicit + + string Write(T val) + where T : Enum + { + return val.ToString(); // ❌ generic + } +} +``` + + ## Preserving usages of the `[EnumExtensions]` attribute The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. diff --git a/docs/NetEscapades.EnumGenerators.Generators.source.md b/docs/NetEscapades.EnumGenerators.Generators.source.md index d1ec2d99..071d4d05 100644 --- a/docs/NetEscapades.EnumGenerators.Generators.source.md +++ b/docs/NetEscapades.EnumGenerators.Generators.source.md @@ -47,6 +47,8 @@ include: enum-usage include: usage-analyzers +include: package-referencing + ## Enabling automatic interception (experimental) include: interceptor-intro @@ -65,6 +67,4 @@ This adds a `` to your project. You can additionally mark the include: interception-config -include: package-referencing - include: preserving-usages diff --git a/docs/NetEscapades.EnumGenerators.Interceptors.md b/docs/NetEscapades.EnumGenerators.Interceptors.md index 1de39910..4ad30cb2 100644 --- a/docs/NetEscapades.EnumGenerators.Interceptors.md +++ b/docs/NetEscapades.EnumGenerators.Interceptors.md @@ -7,7 +7,7 @@ To change this file edit the source file and then run MarkdownSnippets. # ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images/icon_32.png) NetEscapades.EnumGenerators.Interceptors -A source generator interceptor for automatically intercepting calls to `ToString()` on enums, and replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) +An experimental source generator interceptor for automatically intercepting calls to `ToString()` on enums, and replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) > This source generator requires the .NET 8.0.400 SDK. You can target earlier frameworks like .NET Core 3.1 etc, but the _SDK_ must be at least 8.0.400 diff --git a/docs/NetEscapades.EnumGenerators.md b/docs/NetEscapades.EnumGenerators.md index 759f1547..e21ce8fd 100644 --- a/docs/NetEscapades.EnumGenerators.md +++ b/docs/NetEscapades.EnumGenerators.md @@ -27,9 +27,9 @@ To change this file edit the source file and then run MarkdownSnippets. * [Enabling the analyzers](#enabling-the-analyzers) * [Configuring analyzer severity (optional)](#configuring-analyzer-severity-optional) * [Code fixes](#code-fixes) - * [Enabling automatic interception](#enabling-automatic-interception) * [Avoiding runtime dependencies](#avoiding-runtime-dependencies) - * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) + * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) + * [Enabling automatic interception (experimental)](#enabling-automatic-interception-experimental) ## Why use these packages? @@ -414,96 +414,6 @@ All usage analyzers include automatic code fixes. When a diagnostic is triggered ![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) -## Enabling automatic interception (experimental) - -The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). - - -For example, imagine you have this code, which uses the `Color` enum defined above: - -```csharp -var choice = Color.Red; -Console.WriteLine("You chose: " + choice.ToString()); -``` - -By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: - -```csharp -// The compiler replaces the call with this 👇 -Console.WriteLine("You chose: " + choice.ToStringFast()); -``` - -There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. - -Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. - -> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. - -To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: - -```bash -dotnet add package NetEscapades.EnumGenerators.Interceptors -``` - -This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. - -By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. - -```csharp -[assembly:Interceptable] -[assembly:Interceptable] -``` - -If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. - -```csharp -[EnumExtensions(IsInterceptable = false)] -public enum Colour -{ - Red = 0, - Blue = 1, -} -``` - -Interception only works when the target type is unambiguously an interceptable enum, so it won't work - -- When `ToString()` is called in other source generated code. -- When `ToString()` is called in already-compiled code. -- If the `ToString()` call is _implicit_ (for example in `string` interpolation) -- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` -- If the `ToString()` call is made on a generic type - -For example: - -```csharp -// All the examples in this method CAN be intercepted -public void CanIntercept() -{ - var ok1 = Color.Red.ToString(); // ✅ - var red = Color.Red; - var ok2 = red.ToString(); // ✅ - var ok3 = "The colour is " + red.ToString(); // ✅ - var ok4 = $"The colour is {red.ToString()}"; // ✅ -} - -// The examples in this method can NOT be intercepted -public void CantIntercept() -{ - var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type - var bad2 = ((object)Color.Red).ToString(); // ❌ Base type - - var bad3 = "The colour is " + red; // ❌ implicit - var bad4 = $"The colour is {red}"; // ❌ implicit - - string Write(T val) - where T : Enum - { - return val.ToString(); // ❌ generic - } -} -``` - - ## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. @@ -636,6 +546,96 @@ The final option is to reference [NetEscapades.EnumGenerators.Generators](https: > [!WARNING] > When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. +## Enabling automatic interception (experimental) + +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). + + +For example, imagine you have this code, which uses the `Color` enum defined above: + +```csharp +var choice = Color.Red; +Console.WriteLine("You chose: " + choice.ToString()); +``` + +By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: + +```csharp +// The compiler replaces the call with this 👇 +Console.WriteLine("You chose: " + choice.ToStringFast()); +``` + +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. + +Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. + +> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. + +To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: + +```bash +dotnet add package NetEscapades.EnumGenerators.Interceptors +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. + +By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. + +```csharp +[assembly:Interceptable] +[assembly:Interceptable] +``` + +If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. + +```csharp +[EnumExtensions(IsInterceptable = false)] +public enum Colour +{ + Red = 0, + Blue = 1, +} +``` + +Interception only works when the target type is unambiguously an interceptable enum, so it won't work + +- When `ToString()` is called in other source generated code. +- When `ToString()` is called in already-compiled code. +- If the `ToString()` call is _implicit_ (for example in `string` interpolation) +- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` +- If the `ToString()` call is made on a generic type + +For example: + +```csharp +// All the examples in this method CAN be intercepted +public void CanIntercept() +{ + var ok1 = Color.Red.ToString(); // ✅ + var red = Color.Red; + var ok2 = red.ToString(); // ✅ + var ok3 = "The colour is " + red.ToString(); // ✅ + var ok4 = $"The colour is {red.ToString()}"; // ✅ +} + +// The examples in this method can NOT be intercepted +public void CantIntercept() +{ + var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type + var bad2 = ((object)Color.Red).ToString(); // ❌ Base type + + var bad3 = "The colour is " + red; // ❌ implicit + var bad4 = $"The colour is {red}"; // ❌ implicit + + string Write(T val) + where T : Enum + { + return val.ToString(); // ❌ generic + } +} +``` + + ## Preserving usages of the `[EnumExtensions]` attribute The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. diff --git a/docs/NetEscapades.EnumGenerators.source.md b/docs/NetEscapades.EnumGenerators.source.md index 79332d81..c1d03242 100644 --- a/docs/NetEscapades.EnumGenerators.source.md +++ b/docs/NetEscapades.EnumGenerators.source.md @@ -49,7 +49,9 @@ include: enum-usage include: usage-analyzers -## Enabling automatic interception +include: package-referencing + +## Enabling automatic interception (experimental) include: interceptor-intro @@ -67,6 +69,4 @@ This adds a `` to your project. You can additionally mark the include: interception-config -include: package-referencing - include: preserving-usages diff --git a/docs/README.md b/docs/README.md index 457a94e7..06eeec62 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,9 +27,9 @@ To change this file edit the source file and then run MarkdownSnippets. * [Enabling the analyzers](#enabling-the-analyzers) * [Configuring analyzer severity (optional)](#configuring-analyzer-severity-optional) * [Code fixes](#code-fixes) - * [Enabling automatic interception](#enabling-automatic-interception) * [Avoiding runtime dependencies](#avoiding-runtime-dependencies) - * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) + * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) + * [Enabling automatic interception (experimental)](#enabling-automatic-interception-experimental) ## Why use these packages? @@ -414,96 +414,6 @@ All usage analyzers include automatic code fixes. When a diagnostic is triggered ![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) -## Enabling automatic interception - -The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). - - -For example, imagine you have this code, which uses the `Color` enum defined above: - -```csharp -var choice = Color.Red; -Console.WriteLine("You chose: " + choice.ToString()); -``` - -By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: - -```csharp -// The compiler replaces the call with this 👇 -Console.WriteLine("You chose: " + choice.ToStringFast()); -``` - -There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. - -Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. - -> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. - -To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: - -```bash -dotnet add package NetEscapades.EnumGenerators.Interceptors -``` - -This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. - -By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. - -```csharp -[assembly:Interceptable] -[assembly:Interceptable] -``` - -If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. - -```csharp -[EnumExtensions(IsInterceptable = false)] -public enum Colour -{ - Red = 0, - Blue = 1, -} -``` - -Interception only works when the target type is unambiguously an interceptable enum, so it won't work - -- When `ToString()` is called in other source generated code. -- When `ToString()` is called in already-compiled code. -- If the `ToString()` call is _implicit_ (for example in `string` interpolation) -- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` -- If the `ToString()` call is made on a generic type - -For example: - -```csharp -// All the examples in this method CAN be intercepted -public void CanIntercept() -{ - var ok1 = Color.Red.ToString(); // ✅ - var red = Color.Red; - var ok2 = red.ToString(); // ✅ - var ok3 = "The colour is " + red.ToString(); // ✅ - var ok4 = $"The colour is {red.ToString()}"; // ✅ -} - -// The examples in this method can NOT be intercepted -public void CantIntercept() -{ - var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type - var bad2 = ((object)Color.Red).ToString(); // ❌ Base type - - var bad3 = "The colour is " + red; // ❌ implicit - var bad4 = $"The colour is {red}"; // ❌ implicit - - string Write(T val) - where T : Enum - { - return val.ToString(); // ❌ generic - } -} -``` - - ## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. @@ -636,6 +546,96 @@ The final option is to reference [NetEscapades.EnumGenerators.Generators](https: > [!WARNING] > When using the [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) metapackage, it's important you _don't_ set `PrivateAssets=All`. If you want to use `PrivateAssets=All`, use [NetEscapades.EnumGenerators.Generators](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators) for this scenario. +## Enabling automatic interception (experimental) + +The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). + + +For example, imagine you have this code, which uses the `Color` enum defined above: + +```csharp +var choice = Color.Red; +Console.WriteLine("You chose: " + choice.ToString()); +``` + +By default you need to manually replace these `ToString()` calls with `ToStringFast()`. However, when you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: + +```csharp +// The compiler replaces the call with this 👇 +Console.WriteLine("You chose: " + choice.ToStringFast()); +``` + +There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. + +Interceptors were introduced as an experimental feature in C#12 with .NET 8. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. _NetEscapades.EnumGenerators_ has support for intercepting `ToString()` and `HasFlag()` method calls. + +> To use interceptors, you must be using at least version 8.0.400 of the .NET SDK. [This ships with Visual Studio version 17.11](https://learn.microsoft.com/en-us/dotnet/core/porting/versioning-sdk-msbuild-vs), so you will need at least that version or higher. + +To use interception, add the additional NuGet package [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to your project using: + +```bash +dotnet add package NetEscapades.EnumGenerators.Interceptors +``` + +This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`, similarly to _NetEscapades.EnumGenerators_. + +By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. + +```csharp +[assembly:Interceptable] +[assembly:Interceptable] +``` + +If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. + +```csharp +[EnumExtensions(IsInterceptable = false)] +public enum Colour +{ + Red = 0, + Blue = 1, +} +``` + +Interception only works when the target type is unambiguously an interceptable enum, so it won't work + +- When `ToString()` is called in other source generated code. +- When `ToString()` is called in already-compiled code. +- If the `ToString()` call is _implicit_ (for example in `string` interpolation) +- If the `ToString()` call is made on a base type, such as `System.Enum` or `object` +- If the `ToString()` call is made on a generic type + +For example: + +```csharp +// All the examples in this method CAN be intercepted +public void CanIntercept() +{ + var ok1 = Color.Red.ToString(); // ✅ + var red = Color.Red; + var ok2 = red.ToString(); // ✅ + var ok3 = "The colour is " + red.ToString(); // ✅ + var ok4 = $"The colour is {red.ToString()}"; // ✅ +} + +// The examples in this method can NOT be intercepted +public void CantIntercept() +{ + var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type + var bad2 = ((object)Color.Red).ToString(); // ❌ Base type + + var bad3 = "The colour is " + red; // ❌ implicit + var bad4 = $"The colour is {red}"; // ❌ implicit + + string Write(T val) + where T : Enum + { + return val.ToString(); // ❌ generic + } +} +``` + + ## Preserving usages of the `[EnumExtensions]` attribute The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. diff --git a/docs/README.source.md b/docs/README.source.md index 79332d81..c1d03242 100644 --- a/docs/README.source.md +++ b/docs/README.source.md @@ -49,7 +49,9 @@ include: enum-usage include: usage-analyzers -## Enabling automatic interception +include: package-referencing + +## Enabling automatic interception (experimental) include: interceptor-intro @@ -67,6 +69,4 @@ This adds a `` to your project. You can additionally mark the include: interception-config -include: package-referencing - include: preserving-usages From ededcba5044dc56e7b813c7a0580d07f4ef7a6cf Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 6 Mar 2026 22:19:45 +0000 Subject: [PATCH 6/7] Fix ToC generation --- docs/NetEscapades.EnumGenerators.Generators.md | 14 ++++++++++---- docs/NetEscapades.EnumGenerators.md | 14 ++++++++++---- docs/README.md | 14 ++++++++++---- docs/fragments/package-referencing.include.md | 1 + docs/fragments/preserving-usages.include.md | 1 + docs/fragments/usage-analyzers.include.md | 1 + 6 files changed, 33 insertions(+), 12 deletions(-) diff --git a/docs/NetEscapades.EnumGenerators.Generators.md b/docs/NetEscapades.EnumGenerators.Generators.md index 45aa04b9..eacfc75b 100644 --- a/docs/NetEscapades.EnumGenerators.Generators.md +++ b/docs/NetEscapades.EnumGenerators.Generators.md @@ -22,12 +22,15 @@ In general, we recommend installing the [NetEscapades.EnumGenerators](https://ww * [Why use these packages?](#why-use-these-packages) * [Adding NetEscapades.EnumGenerators.Generators to your project](#adding-netescapadesenumgeneratorsgenerators-to-your-project) * [Controlling extension accessibility](#controlling-extension-accessibility) + * [Usage Analyzers](#usage-analyzers) * [Enabling the analyzers](#enabling-the-analyzers) * [Configuring analyzer severity (optional)](#configuring-analyzer-severity-optional) * [Code fixes](#code-fixes) + * [Package referencing options](#package-referencing-options) * [Avoiding runtime dependencies](#avoiding-runtime-dependencies) * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) - * [Enabling automatic interception (experimental)](#enabling-automatic-interception-experimental) + * [Enabling automatic interception (experimental)](#enabling-automatic-interception-experimental) + * [Preserving usages of the `[EnumExtensions]` attribute](#preserving-usages-of-the-enumextensions-attribute) ## Why use these packages? @@ -342,7 +345,8 @@ or for external enums: ``` -## Usage Analyzers + +## Usage Analyzers _NetEscapades.EnumGenerators_ includes optional analyzers that encourage the use of the generated extension methods instead of the built-in `System.Enum` methods. These analyzers can help improve performance by suggesting the faster, generated, alternatives like `ToStringFast()`, `HasFlagFast()`, and `TryParse()`. @@ -412,7 +416,8 @@ All usage analyzers include automatic code fixes. When a diagnostic is triggered ![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) -## Package referencing options + +## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. @@ -634,7 +639,8 @@ public void CantIntercept() ``` -## Preserving usages of the `[EnumExtensions]` attribute + +## Preserving usages of the `[EnumExtensions]` attribute The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. diff --git a/docs/NetEscapades.EnumGenerators.md b/docs/NetEscapades.EnumGenerators.md index e21ce8fd..7f189171 100644 --- a/docs/NetEscapades.EnumGenerators.md +++ b/docs/NetEscapades.EnumGenerators.md @@ -24,12 +24,15 @@ To change this file edit the source file and then run MarkdownSnippets. * [Why use these packages?](#why-use-these-packages) * [Adding NetEscapades.EnumGenerators to your project](#adding-netescapadesenumgenerators-to-your-project) * [Controlling extension accessibility](#controlling-extension-accessibility) + * [Usage Analyzers](#usage-analyzers) * [Enabling the analyzers](#enabling-the-analyzers) * [Configuring analyzer severity (optional)](#configuring-analyzer-severity-optional) * [Code fixes](#code-fixes) + * [Package referencing options](#package-referencing-options) * [Avoiding runtime dependencies](#avoiding-runtime-dependencies) * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) - * [Enabling automatic interception (experimental)](#enabling-automatic-interception-experimental) + * [Enabling automatic interception (experimental)](#enabling-automatic-interception-experimental) + * [Preserving usages of the `[EnumExtensions]` attribute](#preserving-usages-of-the-enumextensions-attribute) ## Why use these packages? @@ -344,7 +347,8 @@ or for external enums: ``` -## Usage Analyzers + +## Usage Analyzers _NetEscapades.EnumGenerators_ includes optional analyzers that encourage the use of the generated extension methods instead of the built-in `System.Enum` methods. These analyzers can help improve performance by suggesting the faster, generated, alternatives like `ToStringFast()`, `HasFlagFast()`, and `TryParse()`. @@ -414,7 +418,8 @@ All usage analyzers include automatic code fixes. When a diagnostic is triggered ![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) -## Package referencing options + +## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. @@ -636,7 +641,8 @@ public void CantIntercept() ``` -## Preserving usages of the `[EnumExtensions]` attribute + +## Preserving usages of the `[EnumExtensions]` attribute The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. diff --git a/docs/README.md b/docs/README.md index 06eeec62..7e238e56 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,12 +24,15 @@ To change this file edit the source file and then run MarkdownSnippets. * [Why use these packages?](#why-use-these-packages) * [Adding NetEscapades.EnumGenerators to your project](#adding-netescapadesenumgenerators-to-your-project) * [Controlling extension accessibility](#controlling-extension-accessibility) + * [Usage Analyzers](#usage-analyzers) * [Enabling the analyzers](#enabling-the-analyzers) * [Configuring analyzer severity (optional)](#configuring-analyzer-severity-optional) * [Code fixes](#code-fixes) + * [Package referencing options](#package-referencing-options) * [Avoiding runtime dependencies](#avoiding-runtime-dependencies) * [Choosing the correct packages for your scenario](#choosing-the-correct-packages-for-your-scenario) - * [Enabling automatic interception (experimental)](#enabling-automatic-interception-experimental) + * [Enabling automatic interception (experimental)](#enabling-automatic-interception-experimental) + * [Preserving usages of the `[EnumExtensions]` attribute](#preserving-usages-of-the-enumextensions-attribute) ## Why use these packages? @@ -344,7 +347,8 @@ or for external enums: ``` -## Usage Analyzers + +## Usage Analyzers _NetEscapades.EnumGenerators_ includes optional analyzers that encourage the use of the generated extension methods instead of the built-in `System.Enum` methods. These analyzers can help improve performance by suggesting the faster, generated, alternatives like `ToStringFast()`, `HasFlagFast()`, and `TryParse()`. @@ -414,7 +418,8 @@ All usage analyzers include automatic code fixes. When a diagnostic is triggered ![Demonstrating the code-fix option available in your IDE for each of the analyzers](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/docs/images//code_fix.png) -## Package referencing options + +## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. @@ -636,7 +641,8 @@ public void CantIntercept() ``` -## Preserving usages of the `[EnumExtensions]` attribute + +## Preserving usages of the `[EnumExtensions]` attribute The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. diff --git a/docs/fragments/package-referencing.include.md b/docs/fragments/package-referencing.include.md index abe515e4..cbd0df7d 100644 --- a/docs/fragments/package-referencing.include.md +++ b/docs/fragments/package-referencing.include.md @@ -1,3 +1,4 @@ + ## Package referencing options [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality. diff --git a/docs/fragments/preserving-usages.include.md b/docs/fragments/preserving-usages.include.md index 6ff2bf55..d81fd058 100644 --- a/docs/fragments/preserving-usages.include.md +++ b/docs/fragments/preserving-usages.include.md @@ -1,3 +1,4 @@ + ## Preserving usages of the `[EnumExtensions]` attribute The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute, [so their usage will not appear in the build output of your project](https://andrewlock.net/conditional-compilation-for-ignoring-method-calls-with-the-conditionalattribute/#applying-the-conditional-attribute-to-classes). If you use reflection at runtime on one of your `enum`s, you will not find `[EnumExtensions]` in the list of custom attributes. If you wish to preserve these attributes in the build output, you can define the `NETESCAPADES_ENUMGENERATORS_USAGES` MSBuild variable. diff --git a/docs/fragments/usage-analyzers.include.md b/docs/fragments/usage-analyzers.include.md index 1496a429..66b13406 100644 --- a/docs/fragments/usage-analyzers.include.md +++ b/docs/fragments/usage-analyzers.include.md @@ -1,3 +1,4 @@ + ## Usage Analyzers _NetEscapades.EnumGenerators_ includes optional analyzers that encourage the use of the generated extension methods instead of the built-in `System.Enum` methods. These analyzers can help improve performance by suggesting the faster, generated, alternatives like `ToStringFast()`, `HasFlagFast()`, and `TryParse()`. From 4d5203883e31a729729531e244781d8a11a3f221 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Sat, 7 Mar 2026 22:23:11 +0000 Subject: [PATCH 7/7] Fix Readme --- .../NetEscapades.EnumGenerators.Generators.csproj | 2 +- .../NetEscapades.EnumGenerators.Interceptors.csproj | 2 +- .../NetEscapades.EnumGenerators.RuntimeDependencies.csproj | 2 +- .../NetEscapades.EnumGenerators.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NetEscapades.EnumGenerators.Generators/NetEscapades.EnumGenerators.Generators.csproj b/src/NetEscapades.EnumGenerators.Generators/NetEscapades.EnumGenerators.Generators.csproj index 7f691182..981cd773 100644 --- a/src/NetEscapades.EnumGenerators.Generators/NetEscapades.EnumGenerators.Generators.csproj +++ b/src/NetEscapades.EnumGenerators.Generators/NetEscapades.EnumGenerators.Generators.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.csproj b/src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.csproj index 7bde1e6c..291a559e 100644 --- a/src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.csproj +++ b/src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/NetEscapades.EnumGenerators.RuntimeDependencies/NetEscapades.EnumGenerators.RuntimeDependencies.csproj b/src/NetEscapades.EnumGenerators.RuntimeDependencies/NetEscapades.EnumGenerators.RuntimeDependencies.csproj index a819875e..fe6caec9 100644 --- a/src/NetEscapades.EnumGenerators.RuntimeDependencies/NetEscapades.EnumGenerators.RuntimeDependencies.csproj +++ b/src/NetEscapades.EnumGenerators.RuntimeDependencies/NetEscapades.EnumGenerators.RuntimeDependencies.csproj @@ -16,7 +16,7 @@ - + \ No newline at end of file diff --git a/src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj b/src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj index 4af0706b..d0abe6b3 100644 --- a/src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj +++ b/src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj @@ -20,7 +20,7 @@ - + \ No newline at end of file