NDepend Blog

Improve your .NET code quality with NDepend

C# Discriminated Union: What’s Driving the C# Community’s Inquiries?

May 6, 2024 6 minutes read

C# Discriminated Union

In recent years, there has been a notable surge in inquiries from the C# community regarding language-level support for C# Discriminated Unions. What individuals truly desire is the ability to express complex data structures more elegantly and effectively.

This blog post offers a thorough introduction to simulating Discriminated Unions (DUs) in C# programs, highlighting their usefulness.

First, let’s delve into DUs in F#.

F# Discriminated Unions

Here is a F# program that illustrates DU:

This program prints:

Vehicle is a Discriminated Union type. At runtime, an instance of Vehicle is a Car, a Truck or a Bicycle. If one of these option type is not handled, the F# compiler emits a warning:

F# Discriminated Union Warning

The F# compiler builds a  .NET DLL that contains IL code. If we decompile the IL code generated we can see that inheritance is used to handle F# DUs and that some helper methods are generated:

F# Discriminated Union Decompiled

Simulating Discriminated Unions in C#

In C# we have a nice support for ValueTuple which are AND types. A person can be modelized through a first name and a last name and a birth year. Here what we want is support for OR type, a Vehicle being a Car, or a Truck or a Bicycle.

The Naive Way

Let’s attempt to rewrite the F# program with inheritance, C# records, and C# Pattern Matching syntax this way:

Not too bad. This C# program is nearly as concise as its F# equivalent. But there are two important disadvantages:

  • In F#, it is unlikely that more option types like Plane would be added to Vehicle. Because the Vehicle type definition would be modified. This would cause compiler warnings or errors everywhere it is used. We say that Vehicle in F# is closed for modification. On the other hand, in C# we can append more Vehicle derived record classes without touching the Vehicle definition nor breaking any code. We say that Vehicle in C# is open for extension. The Open Close Principle (OCP) is an advantage of Object Oriented Programming (OOP). But in this particular context where a DU is not expected to ever change, the OCP is irrelevant and it is error-prone.
  • In the C# switch pattern matching scope, if we forget one of the Vehicle derived type there is no warning. However, if we forget the discard pattern _ => (which is useless here) we get a compiler warning.

C# switch warning

While it’s possible to simulate Discriminated Unions in C# this way, we lack the elegant and bullet proof language support that F# provides.

Harnessing the OneOf<> Library

The OneOf<> library, found on GitHub proposes a more satisfactory approach to implementing Discriminated Unions in C#. Let’s import this library in our .csproj project file:

Here is our program rewritten:

Now we avoid the issues highlighted in the previous C# example.

  • Vehicle is a type alias. We cannot add or remove an option type without modifying its definition.
  • Thanks to the method OneOf<Car, Truck, Bicycle>.Match(Func<Car, string>, Func<Truck, string>, Func<Bicycle, string>), we have to handle all cases.
  • If the option type set is changed, all usages of the method Vehicle.Match() will break at compile time. As a consequence, we cannot avoid refactoring any call which ultimately leads to a less error-prone approach.
  • The OneOf.Match() method call is as concise as the C# switch pattern matching syntax. However, arguably, it is slightly less elegant.

Notice that OneOf<T0, T1, ..., TN> is a structure with N+1 fields. On one hand, this design choice alleviates pressure on the garbage collector since there is no object created on the heap. On the other hand, it also means that if there are numerous option types, the structure’s footprint can grow considerably, potentially leading to performance degradation, as structure values are passed and returned by copy.

The library also proposes OneOfBase<...> classes. This makes it possible to derive from such a class like: class StringOrNumber : OneOfBase<string, int>

Discriminated Unions, Method Result and Value Type

Arguably, Discriminated Unions find their primary usage in succinctly representing method results. Frequently, method calls yield either a value or an error.

Typically, an error is modelized using a value type such as enumerations or structures. Thus, the naive C# implementation faces limitations as inheritance is incompatible with value types. Hopefully, C# generics work perfectly with value type. Thus, here again, the OneOf library shines at handling DU method result value:

Most of the time you will need a simple struct Result<TResult, TError> to handle method result and might not need the entire OneOf library. You can develop yourself this type as suggested in this Nick Chapsas’ video. For that, you can get inspiration from the struct OneOf<T0,T1> implementation.

Also you might think that the example of Vehicle used so far implies that adding more kinds of vehicles could make sense someday. On the other hand, you will never need to change struct Result<TResult, TError> one day.

What About F# And Value Type

Here is our F# initial program with a value option type Int in the DU Vehicle:

Upon decompiling the DLL produced by the F# compiler, we can see that it generates a class Int to enable inheritance despite the value type. This resembles boxing a value type, wherein an object is instantiated on the heap solely to host a value. This issue is circumvented in the implementation of generics with struct OneOf<...,Int> since .NET generics don’t lead to boxing value.

F# Discriminated Union with Value Type Decompiled

C# Team Discussion about C# Discriminated Unions

In September 2023, Mads Torgersen began a discussion to explore the potential development of this feature. In this proposition, Mads is more interested by how the .NET runtime should handle DU types than defining the C# syntax. Unlike the OneOf library that uses generic type with N fields…

…Mads suggests a DU type StringOrInt should be represented this way:

On one hand, we avoid the expense of copying N fields every time the Discriminated Union is passed or returned. But on the other hand, value types are boxed at runtime to fit with the object __value.

Mads concludes that a middle-ground implementation could be achieved. A specific number of bytes (like 8 bytes) alongside the object __value reference would be reserved, allowing for the storage of small values and primitive types without requiring boxing. This is what the ValuePrototype project on github attempts to achieve.

Conclusion

As of today, C# 12 lacks native support for Discriminated Union types. This article showed that using the library OneOf offers advantages over naive attempts to implement Discriminated Union yourself. Let’s emphasize the millions of downloads of the OneOf library, indicating the community’s need for C#’s discriminated union feature.

Hopefully, the C# team is mulling over the optimal approach to implementing Discriminated Unions in C#, suggesting it could eventually become a reality.