LinkDotNet.Enumeration

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: () =&gt; "this is bmw",
    onDacia: () =&gt; "this is dacia",
    onMercedes: () =&gt; "this is mercedes",
    onNone: () =&gt; "this is none",
    onTesla: () =&gt; "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" =&gt; None,
            "Dacia" =&gt; Dacia,
            "Tesla" =&gt; Tesla,
            "BMW" =&gt; BMW,
            "Mercedes" =&gt; Mercedes,
            _ =&gt; 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" =&gt; None,
            "Dacia" =&gt; Dacia,
            "Tesla" =&gt; Tesla,
            "BMW" =&gt; BMW,
            "Mercedes" =&gt; Mercedes,
            _ =&gt; 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" =&gt; None,
            "Dacia" =&gt; Dacia,
            "Tesla" =&gt; Tesla,
            "BMW" =&gt; BMW,
            "Mercedes" =&gt; Mercedes,
            _ =&gt; 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) =&gt; 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) =&gt; TryCreate(key, out _);

    /// <inheritdoc>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static CarTypes Parse(string s, IFormatProvider? provider) =&gt; Create(s);

    /// <inheritdoc>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool TryParse(string? s, IFormatProvider? provider, [NotNullWhen(true)] out CarTypes? result) =&gt; TryCreate(s, out result);

    /// <inheritdoc>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static CarTypes Parse(ReadOnlySpan<char> s, IFormatProvider? provider) =&gt; Create(s.ToString());

    /// <inheritdoc>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [NotNullWhen(true)] out CarTypes? result) =&gt; 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)
        =&gt; a is not null &amp;&amp; b is not null &amp;&amp; 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) =&gt; !(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) =&gt; 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) =&gt; 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() =&gt; 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) =&gt; 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" =&gt; onNone,
            "Dacia" =&gt; onDacia,
            "Tesla" =&gt; onTesla,
            "BMW" =&gt; onBMW,
            "Mercedes" =&gt; onMercedes,
            _ =&gt; 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" =&gt; onNone(),
            "Dacia" =&gt; onDacia(),
            "Tesla" =&gt; onTesla(),
            "BMW" =&gt; onBMW(),
            "Mercedes" =&gt; onMercedes(),
            _ =&gt; 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" =&gt; onNone(state),
            "Dacia" =&gt; onDacia(state),
            "Tesla" =&gt; onTesla(state),
            "BMW" =&gt; onBMW(state),
            "Mercedes" =&gt; onMercedes(state),
            _ =&gt; 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&lt;T&gt; 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) =&gt; (Values, MemberCasing) = (values, Casing.PascalCase);
    public EnumerationAttribute(Casing casing, params string[] values) =&gt; (Values, MemberCasing) = (values, casing);
}

Code and pdf at

https://ignatandrei.github.io/RSCG_Examples/v2/docs/LinkDotNet.Enumeration


Posted

in

,

by

Tags: