Gundi 0.1.0-preview3
dotnet add package Gundi --version 0.1.0-preview3
NuGet\Install-Package Gundi -Version 0.1.0-preview3
<PackageReference Include="Gundi" Version="0.1.0-preview3" />
paket add Gundi --version 0.1.0-preview3
#r "nuget: Gundi, 0.1.0-preview3"
// 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
gundi
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
ofstring
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 Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Newtonsoft.Json (>= 13.0.1)
- System.Text.Json (>= 6.0.2)
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 | 147 | 4/4/2022 |
0.1.0-preview2 | 121 | 4/4/2022 |
0.1.0-preview | 124 | 4/2/2022 |