Discriminated Unions in C#
Hi all. Among the many interesting concepts available in F#, Discriminated Unions attracted me. I wondered how to implement them in C#, because it lacks (syntactic) support for union types, and I decided to find a way to imitate them.
Discriminated Unions – a data type that is a discriminated union, each of which can consist of its own data types (also named).
The idea is that we have limited number of options to choose from, and each option can consist of your datasetno way unrelated with others, but all variants united common subtype.
We will use this idea to create our own Discriminated Unions
Implementation
The “benchmark” will be the implementation in F#
type Worker =
| Developer of KnownLanguages: string seq
| Manager of MaintainedProjectsCount: int
| Tester of UnitTestsPerHour: double
Now implementation in C#
public abstract record RecordWorker
{
private RecordWorker(){ }
public record Developer(IEnumerable<string> KnownLanguages): RecordWorker { }
public record Manager(int MaintainedProjectsCount) : RecordWorker;
public record Tester(double UnitTestsPerHour) : RecordWorker;
}
This implementation fits the criteria described above:
Limited set of choices – all choices – inside another class with a private constructor
Each variant consists of its own set of data – each variant is a separate class
United by a common name/subtype – all inherit the base abstract class
In this implementation, I used record, because they allow you to write less code and are very similar in behavior to Discriminated Unions
Usage
F# function using our type
let getWorkerInfo (worker: Worker) =
match worker with
| Developer knownLanguages ->
$"Known languages: %s{String.Join(',', knownLanguages)}"
| Manager maintainedProjectsCount ->
$"Currently maintained projects count %i{maintainedProjectsCount}"
| Tester unitTestsPerHour ->
$"My testing speed is %f{unitTestsPerHour} unit tests per hour"
In C# it can be rewritten like this
string GetWorkerInfo(Worker w)
{
return worker switch
{
Worker.Developer(var knownLanguages) =>
$"Known languages {string.Join(',', knownLanguages)}",
Worker.Manager(var maintainedProjectsCount) =>
$"Currently maintained projects count {maintainedProjectsCount}",
Worker.Tester(var unitTestsPerHour) =>
$"My testing speed is {unitTestsPerHour} unit tests per hour",
_ =>
throw new ArgumentOutOfRangeException(nameof(worker), worker, null)
};
}
IDE hints become available to us (Rider still swears due to the lack of a default condition)

Comparison of implementations
C# | F# | |
Finding Available Options | IDE (Variants – classes-fields of the base class) | Tags (Enum) |
Implemented Interfaces | IEquatable | IEquatable IStructuralEquatable |
Creating new objects | Constructor | Static method (New*) |
Type definition in runtime | Reflection only | Properties for each option (Is*) |
Generated Properties | Get/Set | Get-only |
Generated Comparison Methods | ==, !=, Equals | Equals |
Recursive definition of Discriminated Unions | Yes, make the choice abstract | No, define another DU above and make it the choice in the current one |
Representation in IL | Base abstract class with implementation options that inherit it | |
Data storage for each option | Properties with a backing field | |
Field deconstruction | There is |
Notes:
conclusions
My version based on records is very similar to the one generated by the F# compiler (even superior in some ways).
There are many implementation options: on ordinary classes, on structures, partial classes.
Also, the advantage of the class implementation is the ability to define common fields – in Discriminated Unions, only the Tag and Is* properties are common to determine the subtype.
If anyone is interested in how Discriminated Unions are arranged in more detail, then there is a post on this topic.
That’s all for me. If I missed important points, please correct me.