Sundew.DiscriminatedUnions by Kim Hugener Ohlsen
NuGet / site data
Details
Info
Name: Sundew.DiscriminatedUnions
Author: Kim Hugener Ohlsen
NuGet: https://www.nuget.org/packages/Sundew.DiscriminatedUnions/
You can find more details at https://github.com/sundews/Sundew.DiscriminatedUnions
Source: https://github.com/sundews/Sundew.DiscriminatedUnions
Author
Kim Hugener Ohlsen

Original Readme
Discriminated Unions
Sundew.DiscriminatedUnions implement discriminated unions for C#, until a future version of C# provides it out of the box. The idea is that this package can be deleted once unions are supported in C#, without requiring changes to switch expressions and statements.
In addition, the project supports dimensional unions through default interface methods (traits). A dimensional union is a union where cases can be reused in any number of unions, by supporting interface unions through the possibility of implementing multiple interface and default interface members.
How it works
A Roslyn analyzer asserts and report errors in case switch statements or switch expression do not handle all cases. C# 8 and 9 already comes with great pattern matching support for evaluation.
In order that the inheritance hierarchy remain closed (All cases in the same assembly), an analyzer ensures that unions are not derived from in referencing assemblies. Similarly all case classes should be sealed.
Create a union by inheriting from an abstract base (record) class (or interface) marked with the DiscriminatedUnion attribute to build various cases. Either specify the partial keyword to the union for a source generator to implement factory methods or use the codefix PDU0001 to generate them.
Sample
######### Defining a union
[Sundew.DiscriminatedUnions.DiscriminatedUnion]
public abstract partial record Result
{
public sealed partial record Success : Result;
public sealed partial record Warning(string Message) : Result;
public sealed partial record Error(int Code) : Result;
}
Alternatively, a union can be defined with unnested case classes and interfaces, allowing the possibility of creating dimensional unions (see below).
######### Evaluation
var message = result switch
{
Result.Error \{ Code: > 70 \} error => $"High Error code: {error.Code}",
Result.Error error => $"Error code: {error.Code}",
Result.Warning \{ Message: "Tough warning" \} => "Not good",
Result.Warning warning => warning.Message,
Result.Success => "Great",
};
######### Dimensional unions To support dimensional unions, unnested cases help because the cases are no longer defined inside a union. However, for this to work the unions are required to declare a factory method named exactly like the case type and that has the CaseType attribute specifying the actual type. Since version 3, factory methods are generated when the union is declared partial. Alternatively, a code fix (PDU0001) is available to generate the factory methods.
[Sundew.DiscriminatedUnions.DiscriminatedUnion]
public partial interface IExpression;
[Sundew.DiscriminatedUnions.DiscriminatedUnion]
public partial interface IArithmeticExpression : IExpression;
[Sundew.DiscriminatedUnions.DiscriminatedUnion]
public partial interface ICommutativeExpression : IArithmeticExpression;
public sealed partial record AdditionExpression(IExpression Lhs, IExpression Rhs) : ICommutativeExpression;
public sealed partial record SubtractionExpression(IExpression Lhs, IExpression Rhs) : IArithmeticExpression;
public sealed partial record MultiplicationExpression(IExpression Lhs, IExpression Rhs) : ICommutativeExpression;
public sealed partial record DivisionExpression(IExpression Lhs, IExpression Rhs) : IArithmeticExpression;
public sealed partial record ValueExpression(int Value) : IExpression;
########## Evaluating dimensional unions With dimensional unions it is possible to handle all cases using a sub union. As seen in the example below, handling the ArithmeticExpression covers Addition-, Subtraction-, Multiplication- and DivisionExpression. Typically one would dispatch these to a method handling ArithmeticExpression and where handling all cases would be checked, but it is not required. This makes it convienient to separate handling logic in smaller chucks of code.
public int Evaluate(Expression expression)
{
return expression switch
{
ArithmeticExpression arithmeticExpression => Evaluate(arithmeticExpression),
ValueExpression valueExpression => valueExpression.Value,
};
}
public int Evaluate(ArithmeticExpression arithmeticExpression)
{
return arithmeticExpression switch
{
AdditionExpression additionExpression => Evaluate(additionExpression.Lhs) + Evaluate(additionExpression.Rhs),
SubtractionExpression subtractionExpression => Evaluate(subtractionExpression.Lhs) - Evaluate(subtractionExpression.Rhs),
MultiplicationExpression multiplicationExpression => Evaluate(multiplicationExpression.Lhs) * Evaluate(multiplicationExpression.Rhs),
DivisionExpression divisionExpression => Evaluate(divisionExpression.Lhs) / Evaluate(divisionExpression.Rhs),
};
}
########## Enum evaluation As of version 5.1, regular enums can also use the DiscriminatedUnion attribute causing the analyzer to exhaustively check switch statements and expressions.
Generator features
As mentioned a source generator is automatically activated for generating factory methods when the partial keyword is specified. In addition, the DiscriminatedUnion attribute can specify a flags enum (GeneratorFeatures) to control additional code generation.
- Segregate - Generates an extension method for IEnumerable<TUnion> that segregates all items into buckets of the different result.
Supported diagnostics:
| Diagnostic Id | Description | Code Fix |
|---|---|---|
| SDU0001 | Switch does not handled all cases | yes |
| SDU0002 | Switch should not handle default case | yes |
| SDU0003 | Switch has unreachable null case | yes |
| SDU0004 | Class unions must be abstract | yes |
| SDU0005 | Only unions can extended other unions | no |
| SDU0006 | Unions cannot be extended outside their assembly | no |
| SDU0007 | Cases must be declared in the same assembly as their unions | no |
| SDU0008 | Cases should be sealed | yes |
| SDU0009 | Unnested cases should have factory method | PDU0001 |
| SDU0010 | Factory method should have correct CaseTypeAttribute | yes |
| SDU0011 | Reported when a case is implemented by throwing NotImplementedException, because CodeCleanup may siliently 'fix' SDU0001. | yes |
| SDU0012 | Reported when a case contains type parameters that are not in the union type parameter list. | yes |
| PDU0001 | Make union/case partial for code generator | yes |
| PDU0002 | Populate union factory methods | yes |
| SDU9999 | Switch should throw in default case | no |
| GDU0001 | Discriminated union declaration could not be found | no |
Issues/Todos
- Switch appears with red squiggly lines in VS: https://github.com/dotnet/roslyn/issues/57041
- Nullability is falsely evaluated when the switch hints null is possible: https://github.com/dotnet/roslyn/issues/57042
About
Generate tagged union
How to use
Example (source csproj, source files)
- CSharp Project
- Program.cs
- ResultSave.cs
- SaveToDatabase.cs
This is the CSharp Project that references Sundew.DiscriminatedUnions
<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 Include="Sundew.DiscriminatedUnions" Version="6.0.0" ></PackageReference>
</ItemGroup>
</Project>
This is the use of Sundew.DiscriminatedUnions in Program.cs
using UnionTypesDemo;
Console.WriteLine("Save or not");
ResultSave data = SaveToDatabase.Save(0);
var message= data switch
{
ResultSave.Ok ok => $"Saved {ok.i}",
ResultSave.NotFound => "Not found",
};
Console.WriteLine(message);
data = SaveToDatabase.Save(1);
message = data switch
{
ResultSave.Ok ok => $"Saved {ok.i}",
ResultSave.NotFound => "Not found",
};
Console.WriteLine(message);
This is the use of Sundew.DiscriminatedUnions in ResultSave.cs
using Sundew.DiscriminatedUnions;
namespace UnionTypesDemo;
[DiscriminatedUnion]
public abstract partial record ResultSave
{
public sealed partial record Ok(int i) : ResultSave;
public sealed partial record NotFound():ResultSave ;
}
This is the use of Sundew.DiscriminatedUnions in SaveToDatabase.cs
namespace UnionTypesDemo;
public class SaveToDatabase
{
public static ResultSave Save(int i)
{
if (i == 0)
{
return ResultSave._NotFound;
}
return ResultSave._Ok(i);
}
}
Generated Files
Those are taken from $(BaseIntermediateOutputPath)\GX
- UnionTypesDemo.ResultSave.generated.cs
- UnionTypesDemo.ResultSave.NotFound.generated.cs
- UnionTypesDemo.ResultSave.Ok.generated.cs
#nullable enable
namespace UnionTypesDemo
{
#pragma warning disable SA1601
[global::System.Diagnostics.DebuggerNonUserCode]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Sundew.DiscriminateUnions.Generator", "6.0.0.0")]
public partial record ResultSave : global::Sundew.DiscriminatedUnions.IDiscriminatedUnion
#pragma warning restore SA1601
{
/// <summary>
/// Gets the NotFound case.
/// </summary>
/// <returns>The NotFound.</returns>
[Sundew.DiscriminatedUnions.CaseType(typeof(global::UnionTypesDemo.ResultSave.NotFound))]
public static global::UnionTypesDemo.ResultSave _NotFound \{ get; }
= new global::UnionTypesDemo.ResultSave.NotFound();
/// <summary>
/// Factory method for the Ok case.
/// </summary>
/// <param name="i">The i.</param>
/// <returns>A new Ok.</returns>
[Sundew.DiscriminatedUnions.CaseType(typeof(global::UnionTypesDemo.ResultSave.Ok))]
public static global::UnionTypesDemo.ResultSave _Ok(int i)
=> new global::UnionTypesDemo.ResultSave.Ok(i);
/// <summary>
/// Gets all cases in the union.
/// </summary>
/// <returns>A readonly list of types.</returns>
public static global::System.Collections.Generic.IReadOnlyList<global::System.Type> Cases \{ get; }
= new global::System.Type[] \{ typeof(global::UnionTypesDemo.ResultSave.NotFound), typeof(global::UnionTypesDemo.ResultSave.Ok) };
}
}
#nullable enable
namespace UnionTypesDemo
{
public partial record ResultSave
{
#pragma warning disable SA1601
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Sundew.DiscriminateUnions.Generator", "6.0.0.0")]
public partial record NotFound
#pragma warning restore SA1601
{
/// <summary>
/// Gets all cases in the union.
/// </summary>
/// <returns>A readonly list of types.</returns>
public new static global::System.Collections.Generic.IReadOnlyList<global::System.Type> Cases \{ get; }
= new global::System.Type[] \{ typeof(global::UnionTypesDemo.ResultSave.NotFound) };
}
}
}
#nullable enable
namespace UnionTypesDemo
{
public partial record ResultSave
{
#pragma warning disable SA1601
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Sundew.DiscriminateUnions.Generator", "6.0.0.0")]
public partial record Ok
#pragma warning restore SA1601
{
/// <summary>
/// Gets all cases in the union.
/// </summary>
/// <returns>A readonly list of types.</returns>
public new static global::System.Collections.Generic.IReadOnlyList<global::System.Type> Cases \{ get; }
= new global::System.Type[] \{ typeof(global::UnionTypesDemo.ResultSave.Ok) };
}
}
}
Useful
Download Example (.NET C#)
Share Sundew.DiscriminatedUnions
https://ignatandrei.github.io/RSCG_Examples/v2/docs/Sundew.DiscriminatedUnions
Category "FunctionalProgramming" has the following generators:
1 cachesourcegenerator
2024-02-14
2 dunet
2023-04-16
3 Dusharp
2024-09-19
4 Funcky.DiscriminatedUnion
2024-01-18
5 FunicularSwitch
2024-02-12
6 N.SourceGenerators.UnionTypes
2023-10-29
7 OneOf
2023-08-21
8 PartiallyApplied
2023-04-16
9 polytype
2024-11-04
10 rscg_demeter
2025-03-26
11 rscg_queryables
2024-11-02
12 RSCG_Utils_Memo
2023-08-27
13 Sera.Union
2024-08-26
14 Sundew.DiscriminatedUnions
2026-02-14
15 TypeUtilities
2024-03-05
16 UnionGen
2024-04-05
17 UnionsGenerator
2024-02-18