Dunet 1.11.0-alpha1

This is a prerelease version of Dunet.
There is a newer version of this package available.
See the version list below for details.
dotnet add package Dunet --version 1.11.0-alpha1                
NuGet\Install-Package Dunet -Version 1.11.0-alpha1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Dunet" Version="1.11.0-alpha1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Dunet --version 1.11.0-alpha1                
#r "nuget: Dunet, 1.11.0-alpha1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Dunet as a Cake Addin
#addin nuget:?package=Dunet&version=1.11.0-alpha1&prerelease

// Install Dunet as a Cake Tool
#tool nuget:?package=Dunet&version=1.11.0-alpha1&prerelease                

Dunet

Build Package

Dunet is a simple source generator for discriminated unions in C#.

Install

  • NuGet: dotnet add package dunet

Usage

// 1. Import the namespace.
using Dunet;

// 2. Add the `Union` attribute to a partial record.
[Union]
partial record Shape
{
    // 3. Define the union variants as inner partial records.
    partial record Circle(double Radius);
    partial record Rectangle(double Length, double Width);
    partial record Triangle(double Base, double Height);
}
// 4. Use the union variants.
var shape = new Shape.Rectangle(3, 4);
var area = shape.Match(
    circle => 3.14 * circle.Radius * circle.Radius,
    rectangle => rectangle.Length * rectangle.Width,
    triangle => triangle.Base * triangle.Height / 2
);
Console.WriteLine(area); // "12"

Generics

Use generics for more advanced union types. For example, an option monad:

// 1. Import the namespace.
using Dunet;
// Optional: use static import for more terse code.
using static Option<int>;

// 2. Add the `Union` attribute to a partial record.
// 3. Add one or more type arguments to the union record.
[Union]
partial record Option<T>
{
    partial record Some(T Value);
    partial record None();
}
// 4. Use the union variants.
Option<int> ParseInt(string? value) =>
    int.TryParse(value, out var number)
        ? new Some(number)
        : new None();

string GetOutput(Option<int> number) =>
    number.Match(
        some => some.Value.ToString(),
        none => "Invalid input!"
    );

var input = Console.ReadLine(); // User inputs "not a number".
var result = ParseInt(input);
var output = GetOutput(result);
Console.WriteLine(output); // "Invalid input!"

input = Console.ReadLine(); // User inputs "12345".
result = ParseInt(input);
output = GetOutput(result);
Console.WriteLine(output); // "12345".

Implicit Conversions

Dunet generates implicit conversions between union variants and the union type if your union meets all of the following conditions:

  • The union has no required properties.
  • All variants contain a single property.
  • Each variant's property is unique within the union.
  • No variant's property is an interface type.

For example, consider a Result union type that represents success as a double and failure as an Exception:

// 1. Import the namespace.
using Dunet;

// 2. Define a union type with a single unique variant property:
[Union]
partial record Result
{
    partial record Success(double Value);
    partial record Failure(Exception Error);
}
// 3. Return union variants directly.
Result Divide(double numerator, double denominator)
{
    if (denominator is 0d)
    {
        // No need for `new Result.Failure(new InvalidOperationException("..."));`
        return new InvalidOperationException("Cannot divide by zero!");
    }

    // No need for `new Result.Success(...);`
    return numerator / denominator;
}

var result = Divide(42, 0);
var output = result.Match(
    success => success.Value.ToString(),
    failure => failure.Error.Message
);

Console.WriteLine(output); // "Cannot divide by zero!"

Async Match

Dunet generates a MatchAsync() extension method for all Task<T> and ValueTask<T> where T is a union type. For example:

// Choice.cs

using Dunet;

namespace Core;

// 1. Define a union type within a namespace.
[Union]
partial record Choice
{
    partial record Yes;
    partial record No(string Reason);
}
// Program.cs

using Core;
using static Core.Choice;

// 2. Define async methods like you would for any other type.
static async Task<Choice> AskAsync()
{
    // Simulating network call.
    await Task.Delay(1000);

    // 3. Return unions from async methods like any other type.
    return new No("because I don't wanna!");
}

// 4. Asynchronously match any union `Task` or `ValueTask`.
var response = await AskAsync()
    .MatchAsync(
        yes => "Yes!!!",
        no => $"No, {no.Reason}"
    );

// Prints "No, because I don't wanna!" after 1 second.
Console.WriteLine(response);

Note: MatchAsync() can only be generated for namespaced unions.

Specific Match

Dunet generates specific match methods for each union variant. This is useful when unwrapping a union and you only care about transforming a single variant. For example:

[Union]
partial record Shape
{
    partial record Point(int X, int Y);
    partial record Line(double Length);
    partial record Rectangle(double Length, double Width);
    partial record Sphere(double Radius);
}
public static bool IsZeroDimensional(this Shape shape) =>
    shape.MatchPoint(
        point => true,
        () => false
    );

public static bool IsOneDimensional(this Shape shape) =>
    shape.MatchLine(
        line => true,
        () => false
    );

public static bool IsTwoDimensional(this Shape shape) =>
    shape.MatchRectangle(
        rectangle => true,
        () => false
    );

public static bool IsThreeDimensional(this Shape shape) =>
    shape.MatchSphere(
        sphere => true,
        () => false
    );

Serialization/Deserialization

using Dunet;
using System.Text.Json.Serialization;

// Serialization and deserialization can be enabled with the `JsonDerivedType` attribute.
[Union]
[JsonDerivedType(typeof(Circle), typeDiscriminator: nameof(Circle))]
[JsonDerivedType(typeof(Rectangle), typeDiscriminator: nameof(Rectangle))]
[JsonDerivedType(typeof(Triangle), typeDiscriminator: nameof(Triangle))]
public partial record Shape
{
    public partial record Circle(double Radius);
    public partial record Rectangle(double Length, double Width);
    public partial record Triangle(double Base, double Height);
}
using System.Text.Json;
using static Shape;

var shapes = new Shape[]
{
    new Circle(10),
    new Rectangle(2, 3),
    new Triangle(2, 1)
};

var serialized = JsonSerializer.Serialize(shapes);

// NOTE: The type discriminator must be the first property in each object.
var deserialized = JsonSerializer.Deserialize<Shape[]>(
    //lang=json
    """
    [
        { "$type": "Circle", "radius": 10 },
        { "$type": "Rectangle", "length": 2, "width": 3 },
        { "$type": "Triangle", "base": 2, "height": 1 }
    ]
    """,
    // So we recognize camelCase properties.
    new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }
);

Pretty Print

To control how union variants are printed with their ToString() methods, override and seal the union declaration's ToString() method. For example:

[Union]
public partial record QueryResult<T>
{
    public partial record Ok(T Value);
    public partial record NotFound;
    public partial record Unauthorized;

    public sealed override string ToString() =>
        Match(
            ok => ok.Value.ToString(),
            notFound => "Not found.",
            unauthorized => "Unauthorized access."
        );
}

Note: You must seal the ToString() override to prevent the compiler from synthesizing a custom ToString() method for each variant.

More info: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#built-in-formatting-for-display

Shared Properties

To create a property shared by all variants, add it to the union declaration. For example, the following code requires all union variants to initialize the StatusCode property. This makes StatusCode available to anyone with a reference to HttpResponse without having to match.

[Union]
public partial record HttpResponse
{
    public partial record Success;
    public partial record Error(string Message);
    // 1. All variants shall have a status code.
    public required int StatusCode { get; init; }
}
using var client = new HttpClient();
var response = await CreateUserAsync(client, "John", "Smith");

// 2. The `StatusCode` property is available at the union level.
var statusCode = response.StatusCode;

public static async Task<HttpResponse> CreateUserAsync(
    HttpClient client, string firstName, string lastName
)
{
    using var response = await client.PostJsonAsync(
        "/users",
        new { firstName, lastName }
    );

    var content = await response.Content.ReadAsStringAsync();

    if (!response.IsSuccessStatusCode)
    {
        return new HttpResponse.Error(content)
        {
            StatusCode = (int)response.StatusCode,
        };
    }

    return new HttpResponse.Success()
    {
        StatusCode = (int)response.StatusCode,
    };
}

Unwrapping

To bypass exhaustive matching and access a variant directly, use the variant-specific Unwrap methods.

This can be useful if you're sure of the underlying value or if you don't care about a potential exception at runtime.

using Dunet;

[Union]
partial record Option<T>
{
    partial record Some(T Value);
    partial record None;
}
Option<double> option1 = new Option<double>.Some(3.14);
var some = option.UnwrapSome();
// You can access `Value` directly here.
Console.WriteLine(some.Value); // Prints "3.14".

Option<double> option2 = new Option<double>.None();
// Throws `InvalidOperationException` because the underlying variant is `None`.
var bad = option.UnwrapSome();

Note: Unwrapping is unsafe. Use only when runtime errors are ok.

Stateful Matching

To reduce memory allocations, use the Match overload that accepts a generic state parameter as its first argument.

This allows your match parameter lambdas to be static but still flow state through:

using Dunet;
using static Expression;

var environment = new Dictionary<string, int>()
{
    ["a"] = 1,
    ["b"] = 2,
    ["c"] = 3,
};

var expression = new Add(new Variable("a"), new Multiply(new Number(2), new Variable("b")));
var result = Evaluate(environment, expression);

Console.WriteLine(result); // "5"

static int Evaluate(Dictionary<string, int> env, Expression exp) =>
    exp.Match(
        // 1. Pass your state "container" as the first parameter.
        state: env,
        // 2. Use static lambdas for each variant's match method.
        static (_, number) => number.Value,
        // 3. Reference the state as the first argument of each lambda.
        static (state, add) => Evaluate(state, add.Left) + Evaluate(state, add.Right),
        static (state, mul) => Evaluate(state, mul.Left) * Evaluate(state, mul.Right),
        static (state, var) => state[var.Value]
    );

[Union]
public partial record Expression
{
    public partial record Number(int Value);
    public partial record Add(Expression Left, Expression Right);
    public partial record Multiply(Expression Left, Expression Right);
    public partial record Variable(string Value);
}

Nest Unions

To declare a union nested within a class or record, the class or record must be partial. For example:

// This type declaration must be partial.
public partial class Parent1
{
    // So must this one.
    public partial class Parent2
    {
        // Unions must always be partial.
        [Union]
        public partial record Nested
        {
            public partial record Variant1;
            public partial record Variant2;
        }
    }
}
// Access variants like any other nested type.
var variant1 = new Parent1.Parent2.Nested.Variant1();

Samples

Product 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 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Dunet:

Package Downloads
ZeroC.Slice

Slice for C#.

Xenial.Identity.Client

Package Description

GitHub repositories (2)

Showing the top 2 popular GitHub repositories that depend on Dunet:

Repository Stars
domn1995/dunet
C# discriminated union source generator
icerpc/icerpc-csharp
A C# RPC framework built for QUIC, with bidirectional streaming, first-class async/await, and Protobuf support.
Version Downloads Last updated
1.11.2 52,637 1/29/2024
1.11.1 315 1/27/2024
1.11.0 8,753 1/4/2024
1.11.0-alpha1 349 1/4/2024
1.10.0 10,842 11/2/2023
1.9.0 4,482 9/13/2023
1.8.0 14,126 5/16/2023
1.7.2-pre1 1,430 5/12/2023
1.7.1 7,675 2/17/2023
1.7.0 1,473 1/23/2023
1.6.0 3,081 1/3/2023
1.6.0-preview1 1,177 1/3/2023
1.5.0 11,332 11/13/2022
1.4.2 73,502 11/5/2022
1.4.1 1,504 11/5/2022
1.4.1-preview2 1,409 11/5/2022
1.4.1-preview1 1,393 11/5/2022
1.4.0 2,025 10/6/2022
1.3.0 1,504 9/22/2022
1.2.0 1,806 8/27/2022
1.1.0 3,262 7/11/2022
1.0.2 1,678 7/8/2022
1.0.1 1,650 7/6/2022
1.0.0 1,063 6/27/2022
0.5.0 908 6/26/2022
0.4.0 908 6/26/2022
0.3.0 915 6/24/2022
0.2.2 889 6/18/2022
0.2.1 926 6/16/2022 0.2.1 is deprecated.
0.2.0 927 6/16/2022 0.2.0 is deprecated.
0.1.3 936 6/15/2022 0.1.3 is deprecated.
0.1.2 909 6/13/2022 0.1.2 is deprecated.
0.1.1 939 6/13/2022 0.1.1 is deprecated.
0.1.0 996 6/13/2022 0.1.0 is deprecated because it is no longer maintained.