Skip to content

AltaSoft.Choice provides the [Choice] attribute to define discriminated union types in C#, enabling source-generated, type-safe choice models.

License

Notifications You must be signed in to change notification settings

altasoft/Choice

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

35 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

AltaSoft.Choice

NuGet - AltaSoft.Choice NuGet - AltaSoft.Choice.Generator Dot NET 8+

AltaSoft.ChoiceGenerator is a lightweight C# source generator that allows you to define choice types (discriminated unions) with minimal syntax.


✨ Features

  • Simple [Choice] attribute for defining alternatives
  • Generates type-safe properties
  • Supports XML and System.Text.Json serialization
  • Includes CreateAsXxx, Match, and Switch methods
  • Auto-generates enum for valid choice types
  • Implicit conversion operators for easy usage
  • Generates XmlSerializer

πŸ› οΈ Installation

Add the following NuGet packages to your project:

<ItemGroup>
  <PackageReference Include="AltaSoft.Choice" Version="x.x.x" />
  <PackageReference Include="AltaSoft.Choice.Generator" Version="x.x.x" PrivateAssets="all" />
</ItemGroup>

βœ… Define Your Choice Type

Mark your class with [Choice] and define partial nullable properties :

using AltaSoft.Choice;
namespace AltaSoft.ChoiceGenerator.Tests;

[Choice]
public sealed partial class Authorisation1Choice
{
    /// <summary>
    /// <para>Specifies the authorisation, in a coded form.</para>
    /// </summary>
    [XmlTag("Cd")]
    [JsonPropertyName("cd")]
    public partial Authorisation1Code? Code { get; set; }

    /// <summary>
    /// <para>Specifies the authorisation, in a free text form.</para>
    /// </summary>
    [XmlTag("Prtry")]
    public partial Proprietary? Proprietary { get; set; }
}

βš™οΈ Generated Code

Below is the generated code for the example above:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by 'AltaSoft Choice.Generator'.
//     Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

#nullable enable

using AltaSoft.ChoiceGenerator.Tests;
using AltaSoft.Choice;
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Schema;

namespace AltaSoft.ChoiceGenerator.Tests;

#pragma warning disable CS8774 // Member must have a non-null value when exiting.
#pragma warning disable CS0628 // New protected member declared in sealed type

public sealed partial record Authorisation1Choice
{
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public Authorisation1Choice()
    {
    }

    /// <summary>
    /// <para>Choice enum </para>
    /// </summary>
    [JsonIgnore]
    [XmlIgnore]
    public ChoiceOf ChoiceType { get; private set; }

    private AltaSoft.ChoiceGenerator.Tests.Authorisation1Code? _code;

    /// <summary>
    /// Specifies the authorisation, in a coded form.
    /// </summary>
    [DisallowNull]
    [XmlElement("Cd")]
    public partial AltaSoft.ChoiceGenerator.Tests.Authorisation1Code? Code
    {
        get => _code;
        set
        {
            _code = value ?? throw new InvalidOperationException("Choice value cannot be null");
            _proprietary = null;
            ChoiceType = ChoiceOf.Code;
        }
    }

    private Proprietary? _proprietary;

    /// <summary>
    /// Specifies the authorisation, in a free text form.
    /// </summary>
    [DisallowNull]
    [XmlElement("Prtry")]
    public partial Proprietary? Proprietary
    {
        get => _proprietary;
        set
        {
            _proprietary = value ?? throw new InvalidOperationException("Choice value cannot be null");
            _code = null;
            ChoiceType = ChoiceOf.Proprietary;
        }
    }


    /// <summary>
    /// Creates a new <see cref="AltaSoft.ChoiceGenerator.Tests.Authorisation1Choice"/> instance and sets its value using the specified <see cref="AltaSoft.ChoiceGenerator.Tests.Authorisation1Code"/>.
    /// </summary>
    /// <param name="value">The value to assign to the created choice instance.</param>
    public static AltaSoft.ChoiceGenerator.Tests.Authorisation1Choice CreateAsCode(AltaSoft.ChoiceGenerator.Tests.Authorisation1Code value) => new () { Code = value };

    /// <summary>
    /// Creates a new <see cref="AltaSoft.ChoiceGenerator.Tests.Authorisation1Choice"/> instance and sets its value using the specified <see cref="Proprietary"/>.
    /// </summary>
    /// <param name="value">The value to assign to the created choice instance.</param>
    public static AltaSoft.ChoiceGenerator.Tests.Authorisation1Choice CreateAsProprietary(Proprietary value) => new () { Proprietary = value };

    /// <summary>
    /// <para>Applies the appropriate function based on the current choice type</para>
    /// </summary>
    /// <typeparam name="TResult">The return type of the provided match functions</typeparam>
    /// <param name="matchCode">Function to invoke if the choice is a <see cref="ChoiceOf.Code"/> value</param>
    /// <param name="matchProprietary">Function to invoke if the choice is a <see cref="ChoiceOf.Proprietary"/> value</param>
    public TResult Match<TResult>(
    	Func<AltaSoft.ChoiceGenerator.Tests.Authorisation1Code, TResult> matchCode, 
    	Func<Proprietary, TResult> matchProprietary)
    {
        return ChoiceType switch
        {
            ChoiceOf.Code => matchCode(Code!.Value),
            ChoiceOf.Proprietary => matchProprietary(Proprietary!),
            _ => throw new InvalidOperationException($"Invalid ChoiceType. '{ChoiceType}'")
        };
    }

    /// <summary>
    /// <para>Applies the appropriate Action based on the current choice type</para>
    /// </summary>
    /// <param name="matchCode">Action to invoke if the choice is a <see cref="ChoiceOf.Code"/> value</param>
    /// <param name="matchProprietary">Action to invoke if the choice is a <see cref="ChoiceOf.Proprietary"/> value</param>
    public void Switch(
    	Action<AltaSoft.ChoiceGenerator.Tests.Authorisation1Code> matchCode, 
    	Action<Proprietary> matchProprietary)
    {
        switch (ChoiceType)
        {
            case ChoiceOf.Code:
                matchCode(Code!.Value);
                return;

            case ChoiceOf.Proprietary:
                matchProprietary(Proprietary!);
                return;

            default:
            throw new XmlException($"Invalid ChoiceType. '{ChoiceType}'");
        }
    }

    /// <summary>
    /// Implicitly converts an <see cref="AltaSoft.ChoiceGenerator.Tests.Authorisation1Code"/> to an <see cref="Authorisation1Choice"/>.
    /// </summary>
    /// <param name="value">The <see cref="AltaSoft.ChoiceGenerator.Tests.Authorisation1Code"/> to convert.</param>
    /// <returns>
    /// <see cref="Authorisation1Choice"/> instance representing the code.
    /// </returns>
    public static implicit operator Authorisation1Choice(AltaSoft.ChoiceGenerator.Tests.Authorisation1Code value) => CreateAsCode(value);

    /// <summary>
    /// Implicitly converts an <see cref="Proprietary"/> to an <see cref="Authorisation1Choice"/>.
    /// </summary>
    /// <param name="value">The <see cref="Proprietary"/> to convert.</param>
    /// <returns>
    /// <see cref="Authorisation1Choice"/> instance representing the code.
    /// </returns>
    public static implicit operator Authorisation1Choice(Proprietary value) => CreateAsProprietary(value);

    /// <summary>
    /// Determines whether the <see cref="Code"/> property should be serialized.
    /// </summary>
    /// <returns>
    /// <c>true</c> if <see cref="Code"/> has a value; otherwise, <c>false</c>.
    /// </returns>
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public bool ShouldSerializeCode() => Code.HasValue;

    /// <summary>
    /// <para>Choice enumeration</para>
    /// </summary>
    [XmlType("ChoiceOf.Authorisation1Choice")]
    public enum ChoiceOf
    {
        /// <summary>
        /// Specifies the authorisation, in a coded form.
        /// </summary>
        Code, 
        /// <summary>
        /// Specifies the authorisation, in a free text form.
        /// </summary>
        Proprietary, 
    }
}

πŸ’‘ Example Usage

Creating with CreateAs methods

var choice = Authorisation1Choice.CreateAsCode(Authorisation1Code.FileLevelAuthorisationSummary);

var result = choice.Match(
    code => $"It's a code: {code}",
    prop => $"It's proprietary: {prop.ToString()}"
);

choice.Switch(
    code => Console.WriteLine($"String: {code}"),
    prop => Console.WriteLine($"Number: {prop.ToString()}")
);

Creating using implicit operators

if property types are distinct implicit operators are generated

Authosiration1Choice choice = Authorisation1Code.FileLevelAuthorisationSummary;

πŸ“¦ Projects

  • AltaSoft.Choice
    Contains the [Choice] marker attribute

  • AltaSoft.Choice.Generator
    Implements the source generator that produces boilerplate code


πŸ“„ License

This project is licensed under the MIT License.

About

AltaSoft.Choice provides the [Choice] attribute to define discriminated union types in C#, enabling source-generated, type-safe choice models.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •