RSCG – LinkDotNet.Enumeration
| name | LinkDotNet.Enumeration |
| nuget | https://www.nuget.org/packages/LinkDotNet.Enumeration/ |
| link | https://github.com/linkdotnet/Enumeration |
| author | Steven Giesel |
Good for replacing enum + switch patterns with string-based enumerations with exhaustive pattern matching.
### Purpose
A source code generator that creates string-based enumerations (similar to Java enums / DDD value objects) with exhaustive pattern matching, replacing enum + switch patterns.
### How to Define
[Enumeration(Casing.Preserve, “None”, “Dacia”, “Tesla”, “BMW”, “Mercedes”)]
public sealed partial record CarTypes;
### How to Use
CarTypes.TryParse(“BMW”, null, out var car);
car.Match(onBMW: () => “this is bmw”, onDacia: () => “this is dacia”, …);
### Key Features
– Exhaustive matching: Match() requires all values
– Create / TryCreate: throws vs returns bool
– IParsable: Minimal APIs & Model Binding
– Implicit string conversion
– CarTypes.All returns FrozenSet of CarTypes
– JSON: GenerateJsonConverter = true
This is how you can use LinkDotNet.Enumeration .
The code that you start with is
<project sdk="Microsoft.NET.Sdk">
<propertygroup>
<outputtype>Exe</outputtype>
<targetframework>net10.0</targetframework>
<implicitusings>enable</implicitusings>
<nullable>enable</nullable>
</propertygroup>
<propertygroup>
<emitcompilergeneratedfiles>true</emitcompilergeneratedfiles>
<compilergeneratedfilesoutputpath>$(BaseIntermediateOutputPath)\GX</compilergeneratedfilesoutputpath>
</propertygroup>
<itemgroup>
<packagereference version="1.5.0" include="LinkDotNet.Enumeration">
</packagereference>
</itemgroup>
The code that you will use is
// See https://aka.ms/new-console-template for more information
using EnumDemo;
using System.Reflection;
Console.WriteLine("Hello, World!");
if(!CarTypes.TryParse("BMW", null,out var car))
{
Console.WriteLine("Invalid car type");
return;
}
var message = car.Match(
onBMW: () => "this is bmw",
onDacia: () => "this is dacia",
onMercedes: () => "this is mercedes",
onNone: () => "this is none",
onTesla: () => "this is tesla"
);
Console.WriteLine(message);
using LinkDotNet.Enumeration; namespace EnumDemo; [Enumeration(Casing.Preserve,"None", "Dacia", "Tesla", "BMW", "Mercedes")] public sealed partial record CarTypes;
The code that is generated is
// <auto-generated>
#nullable enable
using System;
using System.Collections.Frozen;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace EnumDemo;
[DebuggerDisplay("{Key}")]
public sealed partial record CarTypes : IParsable<cartypes>, ISpanParsable<cartypes>, ISpanFormattable
{
/// <summary>Gets the string key that identifies this enumeration value.</summary>
public string Key { get; init; } = default!;
private CarTypes(string key)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);
Key = key;
}
/// <summary>Gets the <see cref="CarTypes"> instance for <c>None</c>.</see></summary>
public static readonly CarTypes None = new("None");
/// <summary>Gets the <see cref="CarTypes"> instance for <c>Dacia</c>.</see></summary>
public static readonly CarTypes Dacia = new("Dacia");
/// <summary>Gets the <see cref="CarTypes"> instance for <c>Tesla</c>.</see></summary>
public static readonly CarTypes Tesla = new("Tesla");
/// <summary>Gets the <see cref="CarTypes"> instance for <c>BMW</c>.</see></summary>
public static readonly CarTypes BMW = new("BMW");
/// <summary>Gets the <see cref="CarTypes"> instance for <c>Mercedes</c>.</see></summary>
public static readonly CarTypes Mercedes = new("Mercedes");
/// <summary>Gets a frozen set of all valid <see cref="CarTypes"> instances. Ordering is not guaranteed.</see></summary>
public static FrozenSet<cartypes> All { get; } =
new CarTypes[] { None, Dacia, Tesla, BMW, Mercedes }.ToFrozenSet();
/// <summary>Creates the <see cref="CarTypes"> instance matching <paramref name="key">.</paramref></see></summary>
/// <param name="key">The key to look up. Must not be null, empty, or whitespace.
/// <returns>The matching <see cref="CarTypes"> instance.</see>
/// <exception cref="ArgumentException">Thrown when <paramref name="key"> is null, empty, or whitespace.</paramref>
/// <exception cref="InvalidOperationException">Thrown when <paramref name="key"> does not match any known value.</paramref>
public static CarTypes Create(string key)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);
return key switch
{
"None" => None,
"Dacia" => Dacia,
"Tesla" => Tesla,
"BMW" => BMW,
"Mercedes" => Mercedes,
_ => throw new InvalidOperationException($"{key} is not a valid value for CarTypes")
};
}
/// <summary>Tries to create the <see cref="CarTypes"> instance matching <paramref name="key">.</paramref></see></summary>
/// <param name="key">The key to look up.
/// <param name="value">When this method returns <see langword="true">, contains the matching <see cref="CarTypes"> instance; otherwise <see langword="null">.
/// <returns><see langword="true"> if a matching instance was found; otherwise <see langword="false">.</see>
public static bool TryCreate(string? key, [NotNullWhen(true)] out CarTypes? value)
{
value = key switch
{
"None" => None,
"Dacia" => Dacia,
"Tesla" => Tesla,
"BMW" => BMW,
"Mercedes" => Mercedes,
_ => null
};
return value is not null;
}
/// <summary>Tries to create the <see cref="CarTypes"> instance matching <paramref name="key"> without allocating a string.</paramref></see></summary>
/// <param name="key">The key span to look up.
/// <param name="value">When this method returns <see langword="true">, contains the matching <see cref="CarTypes"> instance; otherwise <see langword="null">.
/// <returns><see langword="true"> if a matching instance was found; otherwise <see langword="false">.</see>
public static bool TryCreate(ReadOnlySpan<char> key, [NotNullWhen(true)] out CarTypes? value)
{
value = key switch
{
"None" => None,
"Dacia" => Dacia,
"Tesla" => Tesla,
"BMW" => BMW,
"Mercedes" => Mercedes,
_ => null
};
return value is not null;
}
/// <summary>Returns <see langword="true"> if <paramref name="key"> is a valid enumeration value.</paramref></see></summary>
/// <param name="key">The key to check.
/// <returns><see langword="true"> if the key is defined; otherwise <see langword="false">.</see>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDefined(string? key) => TryCreate(key, out _);
/// <summary>Returns <see langword="true"> if <paramref name="key"> is a valid enumeration value without allocating a string.</paramref></see></summary>
/// <param name="key">The key span to check.
/// <returns><see langword="true"> if the key is defined; otherwise <see langword="false">.</see>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDefined(ReadOnlySpan<char> key) => TryCreate(key, out _);
/// <inheritdoc>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static CarTypes Parse(string s, IFormatProvider? provider) => Create(s);
/// <inheritdoc>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParse(string? s, IFormatProvider? provider, [NotNullWhen(true)] out CarTypes? result) => TryCreate(s, out result);
/// <inheritdoc>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static CarTypes Parse(ReadOnlySpan<char> s, IFormatProvider? provider) => Create(s.ToString());
/// <inheritdoc>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [NotNullWhen(true)] out CarTypes? result) => TryCreate(s, out result);
/// <summary>Returns <see langword="true"> when <paramref name="a">'s key equals <paramref name="b"> using ordinal string comparison.</paramref></paramref></see></summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(CarTypes? a, string? b)
=> a is not null && b is not null && a.Key.Equals(b, StringComparison.Ordinal);
/// <summary>Returns <see langword="true"> when <paramref name="a">'s key does not equal <paramref name="b">.</paramref></paramref></see></summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CarTypes? a, string? b) => !(a == b);
/// <summary>Implicitly converts the <see cref="CarTypes"> instance to its <see cref="Key">.</see></see></summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator string(CarTypes value) => value.Key;
/// <summary>Explicitly converts the <see cref="string"> to a <see cref="CarTypes"> instance.</see></see></summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator CarTypes(string key) => Create(key);
/// <summary>Returns the key of this enumeration value.</summary>
/// <returns>The <see cref="Key"> string.</see>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => Key;
/// <inheritdoc>
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
{
if (Key.AsSpan().TryCopyTo(destination))
{
charsWritten = Key.Length;
return true;
}
charsWritten = 0;
return false;
}
/// <inheritdoc>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ToString(string? format, IFormatProvider? provider) => Key;
/// <summary>Returns the value corresponding to the current enumeration value.</summary>
/// <param name="onNone">Returned when the current value is <see cref="None">.
/// <param name="onDacia">Returned when the current value is <see cref="Dacia">.
/// <param name="onTesla">Returned when the current value is <see cref="Tesla">.
/// <param name="onBMW">Returned when the current value is <see cref="BMW">.
/// <param name="onMercedes">Returned when the current value is <see cref="Mercedes">.
/// <typeparam name="T">The return type.</typeparam>
/// <returns>The matched value.</returns>
/// <exception cref="InvalidOperationException">Thrown when no case matches.</exception>
public T Match<t>(T onNone, T onDacia, T onTesla, T onBMW, T onMercedes)
{
return Key switch
{
"None" => onNone,
"Dacia" => onDacia,
"Tesla" => onTesla,
"BMW" => onBMW,
"Mercedes" => onMercedes,
_ => throw new InvalidOperationException($"Unhandled enumeration value: {Key}")
};
}
/// <summary>Invokes the function corresponding to the current value and returns its result.</summary>
/// <param name="onNone">Invoked when the current value is <see cref="None">.
/// <param name="onDacia">Invoked when the current value is <see cref="Dacia">.
/// <param name="onTesla">Invoked when the current value is <see cref="Tesla">.
/// <param name="onBMW">Invoked when the current value is <see cref="BMW">.
/// <param name="onMercedes">Invoked when the current value is <see cref="Mercedes">.
/// <typeparam name="T">The return type.</typeparam>
/// <returns>The value returned by the matched function.</returns>
/// <exception cref="InvalidOperationException">Thrown when no case matches.</exception>
public T Match<t>(Func<t> onNone, Func<t> onDacia, Func<t> onTesla, Func<t> onBMW, Func<t> onMercedes)
{
return Key switch
{
"None" => onNone(),
"Dacia" => onDacia(),
"Tesla" => onTesla(),
"BMW" => onBMW(),
"Mercedes" => onMercedes(),
_ => throw new InvalidOperationException($"Unhandled enumeration value: {Key}")
};
}
/// <summary>Invokes the function corresponding to the current value, passing <paramref name="state">, and returns its result.</paramref></summary>
/// <param name="state">The state to pass to the function.
/// <param name="onNone">Invoked when the current value is <see cref="None">.
/// <param name="onDacia">Invoked when the current value is <see cref="Dacia">.
/// <param name="onTesla">Invoked when the current value is <see cref="Tesla">.
/// <param name="onBMW">Invoked when the current value is <see cref="BMW">.
/// <param name="onMercedes">Invoked when the current value is <see cref="Mercedes">.
/// <typeparam name="T">The return type.</typeparam>
/// <typeparam name="TState">The type of the state object.</typeparam>
/// <returns>The value returned by the matched function.</returns>
/// <exception cref="InvalidOperationException">Thrown when no case matches.</exception>
public T Match<t , tstate="">(TState state, Func<tstate , t=""> onNone, Func<tstate , t=""> onDacia, Func<tstate , t=""> onTesla, Func<tstate , t=""> onBMW, Func<tstate , t=""> onMercedes)
{
return Key switch
{
"None" => onNone(state),
"Dacia" => onDacia(state),
"Tesla" => onTesla(state),
"BMW" => onBMW(state),
"Mercedes" => onMercedes(state),
_ => throw new InvalidOperationException($"Unhandled enumeration value: {Key}")
};
}
/// <summary>Invokes the action corresponding to the current value.</summary>
/// <param name="onNone">Invoked when the current value is <see cref="None">.
/// <param name="onDacia">Invoked when the current value is <see cref="Dacia">.
/// <param name="onTesla">Invoked when the current value is <see cref="Tesla">.
/// <param name="onBMW">Invoked when the current value is <see cref="BMW">.
/// <param name="onMercedes">Invoked when the current value is <see cref="Mercedes">.
/// <exception cref="InvalidOperationException">Thrown when no case matches.</exception>
public void Match(Action onNone, Action onDacia, Action onTesla, Action onBMW, Action onMercedes)
{
switch (Key)
{
case "None": onNone(); return;
case "Dacia": onDacia(); return;
case "Tesla": onTesla(); return;
case "BMW": onBMW(); return;
case "Mercedes": onMercedes(); return;
default: throw new InvalidOperationException($"Unhandled enumeration value: {Key}");
}
}
/// <summary>Invokes the action corresponding to the current value, passing <paramref name="state">.</paramref></summary>
/// <param name="state">The state to pass to the action.
/// <param name="onNone">Invoked when the current value is <see cref="None">.
/// <param name="onDacia">Invoked when the current value is <see cref="Dacia">.
/// <param name="onTesla">Invoked when the current value is <see cref="Tesla">.
/// <param name="onBMW">Invoked when the current value is <see cref="BMW">.
/// <param name="onMercedes">Invoked when the current value is <see cref="Mercedes">.
/// <typeparam name="TState">The type of the state object.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when no case matches.</exception>
public void Match<tstate>(TState state, Action<tstate> onNone, Action<tstate> onDacia, Action<tstate> onTesla, Action<tstate> onBMW, Action<tstate> onMercedes)
{
switch (Key)
{
case "None": onNone(state); return;
case "Dacia": onDacia(state); return;
case "Tesla": onTesla(state); return;
case "BMW": onBMW(state); return;
case "Mercedes": onMercedes(state); return;
default: throw new InvalidOperationException($"Unhandled enumeration value: {Key}");
}
}
}
// <auto-generated>
#nullable enable
using System;
namespace LinkDotNet.Enumeration;
/// <summary>Specifies how static member names are derived from the string values.</summary>
internal enum Casing
{
/// <summary>Converts each value to PascalCase (default).</summary>
PascalCase,
/// <summary>Uses the raw string value as the member name.</summary>
Preserve
}
/// <summary>
/// Marks a <c>partial class</c> or <c>partial record</c> as a source-generated enumeration.
/// The generator emits: Key property, private constructor, static readonly fields,
/// All, Create, TryCreate, == / != operators, ToString, Match<T> and Match(Action).
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
internal sealed class EnumerationAttribute : Attribute
{
public string[] Values { get; }
public Casing MemberCasing { get; }
/// <summary>
/// When <see langword="true">, a <see cref="System.Text.Json.Serialization.JsonConverterAttribute">
/// is applied to the enumeration type and a <c>{TypeName}JsonConverter</c> class is generated
/// that serializes and deserializes the type as its <c>Key</c> string using
/// <see cref="System.Text.Json.JsonSerializer">.
/// </see></see></see></summary>
public bool GenerateJsonConverter { get; init; }
public EnumerationAttribute(params string[] values) => (Values, MemberCasing) = (values, Casing.PascalCase);
public EnumerationAttribute(Casing casing, params string[] values) => (Values, MemberCasing) = (values, casing);
}
Code and pdf at
https://ignatandrei.github.io/RSCG_Examples/v2/docs/LinkDotNet.Enumeration