Gundi 0.1.0-preview3

.NET Standard 2.0
This is a prerelease version of Gundi.
Install-Package Gundi -Version 0.1.0-preview3
dotnet add package Gundi --version 0.1.0-preview3
<PackageReference Include="Gundi" Version="0.1.0-preview3" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Gundi --version 0.1.0-preview3
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: Gundi, 0.1.0-preview3"
#r directive can be used in F# Interactive, C# scripting and .NET Interactive. Copy this into the interactive tool or source code of the script to reference the package.
// Install Gundi as a Cake Addin
#addin nuget:?package=Gundi&version=0.1.0-preview3&prerelease

// Install Gundi as a Cake Tool
#tool nuget:?package=Gundi&version=0.1.0-preview3&prerelease
The NuGet Team does not provide support for this client. Please contact its maintainers for support.

gundi GitHub

A generator for discriminated union for C# (based on union in F#)

Overview

A union is a value that represents several different cases of a different name or/and type. It is useful for modeling more complex choice types - to express that provided data could be shaped differently. For example, Union TxtOrNumber of a string txt and int number informs, that type can contain a string or int, but never both.

Defining a union

In order to use a generator, there has to be a defined simple union schema:

using Gundi;

namespace MyNamespace

[Union] // Mandatory attribute
public partial record SimpleUnion
{
    static partial void Cases(int a, string b, decimal c, int? d); // Mandatory function. `static partial void Cases` is a must-have
}

The Union attribute applies for a partial record (struct and class is NOT ALLOWED). It helps the generator, to identify which types should be enhanced. The Cases method uses specified types and argument names to define union cases.

The generator will generate a partial record which will contain all arguments kept as a private field and some public API:


namespace Gundi.Tests
{
    partial record SimpleUnion
    {
        // tag which contain info about chosen  case
        private readonly byte tag;
        
        // private "case" fields
        private readonly System.Int32 a;
        private readonly System.String b;
        private readonly System.Decimal c;
        private readonly System.Int32? d;
        
        // public API like Map and Match methods
        // (..)
    }
}

Using

Generated union should contain static argument-named factory function, and simple match & map methods:

var union = SimpleUnion.A(5);
Console.WriteLine(union.IsA()); // prints true
Console.WriteLine(union.IsB()); // prints false

var case = union.Match(a => a.ToString(), b => b, c => c.ToString(), d => d.ToString());
Console.WriteLine(case); // prints 5

var mapped = union.Map(
    a => a + 1,
    b => b[..2],
    c => c + 3,
    d => d + 4);
    
var mappedCase = mapped.Match(a => a.ToString(), b => b, c => c.ToString(), d => d.ToString());
Console.WriteLine(mappedCase); // prints 6

Union casting

The generator will generate Cast functions, which "force" to get a defined union case or throws an exception. By default, InvalidOperationException is thrown, but there is a possibility to override the type with CustomCastException setting:

[Union(CustomCastException = typeof(MyException))]
public partial record UnionWithCustomException
{
    static partial void Cases(int a, string b);
}

The selected type must be an exception with a constructor with three arguments

public class MyException : Exception
{
    public MyException(
        Type unionType,      // First argument with union type.
        string expectedCase, // Second with a name of an expected case.
        string actualCase)   // Third with a name of a actual case.
        : base($"Wrong {unionType.Name} cast. Expected: {expectedCase}, Actual: {actualCase}")
    {
    }
}

Serialization

Due to generating fields and constructor with private modifier, the record can't be deserialized as it is. To resolve this, Gundi provides custom JSON converters which are registered via JsonConverter attribute by default:

[System.Text.Json.Serialization.JsonConverter(typeof(UnionJsonConverterFactory))] // assigned by default
[Newtonsoft.Json.JsonConverter(typeof(UnionJsonNetConverter))] // assigned by default
public partial record SimpleUnion
using System.Text.Json;

// (...)

var union = SimpleUnion.A(5);
var json = JsonSerializer.Serialize(union, options);
Console.WriteLine(json); // prints {"Case":"A","Fields":[5]}

var deserialized = JsonSerializer.Deserialize<SimpleUnion>(json, options);
Console.WriteLine(union == deserialized); // prints true
using System.Text.Json;

// (...)

var union = SimpleUnion.A(5);
var json = JsonSerializer.Serialize(union, options);
Console.WriteLine(json); // prints {"Case":"A","Fields":[5]}

var deserialized = JsonSerializer.Deserialize<SimpleUnion>(json, options);
Console.WriteLine(union == deserialized); // prints true

If for some reason, you want to disable automatic converter registration, you can use IgnoreJsonConverterAttribute:

[Union(IgnoreJsonConverterAttribute = true)]
public partial record UnionWithIgnoredJsonAttribute
{
    static partial void Cases((int, string) a, string b);
}
using System.Text.Json;
using Newtonsoft.Json;

// (...)

// "{"Case":"A","Fields":[{"Item1":5,"Item2":"txt"}]}";
var json = "{\"Case\":\"A\",\"Fields\":[{\"Item1\":5,\"Item2\":\"txt\"}]}";

// System.Text.Json:
var options = new JsonSerializerOptions()
{
    Converters = {new UnionJsonConverterFactory()},
    IncludeFields = true // mandatory if tuple is serialized
};

JsonSerializer.Deserialize<UnionWithIgnoredJsonAttribute>(json, options); // works
JsonSerializer.Deserialize<UnionWithIgnoredJsonAttribute>(json); // throws an error

// Newtonsoft.Json:
var settings = new JsonSerializerSettings();
settings.Converters.Add(new UnionJsonNetConverter());

JsonConvert.DeserializeObject<UnionWithIgnoredJsonAttribute>(json, settings); // works
JsonConvert.DeserializeObject<UnionWithIgnoredJsonAttribute>(json); // throws an error

The serialization model is a composition of two values:

  • Case of string for chosen case information,
  • Fields array to keep the value of a case.

The model is compatible with Newtonosoft.Json's F# union converter and allows for deserializing F# union's JSON directly into generated union:

type FRecord = { X: int; Y: string }
type MyFsharpUnion =
    | A of int          // supports simple type
    | B of string       // supports string
    | F of FRecord      // supports F# record
    | T of string * int // DOES NOT SUPPORT F# tuple
[Union]
public partial record CSharpUnion
{
    static partial void Cases(int a, string b, FRecord f, (string, int) t);
}
using Newtonsoft.Json;

// (...)

var fUnion = MyFsharpUnion.NewA(5);
var json = JsonConvert.SerializeObject(fUnion);
Console.WriteLine(json); // prints {"Case":"A","Fields":[5]}

var output = JsonConvert.DeserializeObject<CSharpUnion>(json);
Console.WriteLine(output!.CastToA() == 5); // prints true

NOTE: F# tuple is NOT supported currently.

License

Licensed under the MIT License.

Product Versions
.NET net5.0 net5.0-windows net6.0 net6.0-android net6.0-ios net6.0-maccatalyst net6.0-macos net6.0-tvos net6.0-windows
.NET Core netcoreapp2.0 netcoreapp2.1 netcoreapp2.2 netcoreapp3.0 netcoreapp3.1
.NET Standard netstandard2.0 netstandard2.1
.NET Framework net461 net462 net463 net47 net471 net472 net48
MonoAndroid monoandroid
MonoMac monomac
MonoTouch monotouch
Tizen tizen40 tizen60
Xamarin.iOS xamarinios
Xamarin.Mac xamarinmac
Xamarin.TVOS xamarintvos
Xamarin.WatchOS xamarinwatchos
Compatible target framework(s)
Additional computed target framework(s)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.1.0-preview3 54 4/4/2022
0.1.0-preview2 44 4/4/2022
0.1.0-preview 53 4/2/2022