Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"markdownsnippets.tool": {
"version": "28.0.1",
"commands": [
"mdsnippets"
],
"rollForward": false
}
}
}
30 changes: 12 additions & 18 deletions .github/workflows/BuildAndPack.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,3 @@
# ------------------------------------------------------------------------------
# <auto-generated>
#
# 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
#
# </auto-generated>
# ------------------------------------------------------------------------------

name: BuildAndPack

on:
Expand Down Expand Up @@ -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
Expand All @@ -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
run: ./build.cmd Clean GenerateReadmes 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:
Expand Down
8 changes: 8 additions & 0 deletions build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@ class Build : NukeBuild
}
});

Target GenerateReadmes => _ => _
.Before(Pack)
.Executes(() =>
{
DotNetToolRestore();
DotNet("tool run mdsnippets", RootDirectory / "docs");
});

Target PushToNuGet => _ => _
.DependsOn(Pack)
.OnlyWhenStatic(() => IsTag && IsServerBuild && IsWin)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
<!--
GENERATED FILE - DO NOT EDIT
This file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).
Source File: /NetEscapades.EnumGenerators.Generators.source.md
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.Generators

![Build status](https://github.com/andrewlock/NetEscapades.EnumGenerators/actions/workflows/BuildAndPack.yml/badge.svg)
Expand All @@ -9,9 +16,25 @@ 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 -->
## 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)
* [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)
* [Preserving usages of the `[EnumExtensions]` attribute](#preserving-usages-of-the-enumextensions-attribute)<!-- endToc -->

## 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.<!-- include: benchmark. path: /fragments/benchmark.include.md -->


For example, the following benchmark shows the advantage of calling `ToStringFast()` over `ToString()`:
Expand Down Expand Up @@ -44,25 +67,7 @@ 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.
<!-- endInclude -->

## Adding NetEscapades.EnumGenerators.Generators to your project

Expand All @@ -85,14 +90,14 @@ This adds a `<PackageReference>` to your project. You can additionally mark the
</PropertyGroup>

<!-- Add the package -->
<PackageReference Include="NetEscapades.EnumGenerators.Generators" Version="1.0.0-beta20"
<PackageReference Include="NetEscapades.EnumGenerators.Generators" Version="1.0.0-beta20"
PrivateAssets="all" ExcludeAssets="runtime" />
<!-- -->

</Project>
```

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.<!-- include: enum-usage. path: /fragments/enum-usage.include.md -->

To use the generator, add the `[EnumExtensions]` attribute to an enum. For example:

Expand All @@ -102,7 +107,7 @@ public enum MyEnum
{
First,

[Display(Name = "2nd")]
[EnumMember(Value = "2nd")]
Second,
}
```
Expand Down Expand Up @@ -160,7 +165,7 @@ public static partial class MyEnumExtensions
return true;
}


return name switch
{
nameof(MyEnum.First) => true,
Expand All @@ -181,7 +186,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)
Expand Down Expand Up @@ -331,13 +336,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<ExternalEnum>(IsInternal = true)]
```
<!-- endInclude -->

<!-- include: usage-analyzers. path: /fragments/usage-analyzers.include.md -->
## 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()`.
Expand All @@ -363,7 +371,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}]
Expand Down Expand Up @@ -406,8 +414,9 @@ 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)<!-- endInclude -->

<!-- include: package-referencing. path: /fragments/package-referencing.include.md -->
## Package referencing options

[NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is a metapackage that references additional packages for functionality.
Expand Down Expand Up @@ -460,12 +469,12 @@ namespace NetEscapades.EnumGenerators;
public readonly struct EnumParseOptions { }

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

/// <summary>
/// Transform to apply when calling <c>ToStringFast</c>
/// Transform to apply when calling <c>ToStringFast</c>
/// </summary>
public enum SerializationTransform
```
Expand All @@ -479,7 +488,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
Expand Down Expand Up @@ -538,9 +547,28 @@ 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.<!-- endInclude -->

## 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).<!-- include: interceptor-intro. path: /fragments/interceptor-intro.include.md -->


## Enabling automatic interception
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.<!-- endInclude -->

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.

Expand All @@ -554,7 +582,7 @@ dotnet add package NetEscapades.EnumGenerators.Interceptors

This adds a `<PackageReference>` 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<T>]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable<T>]` 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<T>]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable<T>]` attribute in the project where you want the interception to happen, e.g.<!-- include: interception-config. path: /fragments/interception-config.include.md -->

```csharp
[assembly:Interceptable<DateTimeKind>]
Expand Down Expand Up @@ -598,7 +626,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

Expand All @@ -609,7 +637,9 @@ public void CantIntercept()
}
}
```
<!-- endInclude -->

<!-- include: preserving-usages. path: /fragments/preserving-usages.include.md -->
## 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.
Expand All @@ -628,3 +658,4 @@ The `[EnumExtensions]` attribute is decorated with the `[Conditional]` attribute
<PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta20" />
</Project>
```
<!-- endInclude -->
Loading
Loading