Discriminated Unions in C#

Edit on GitHub

Discriminated unions have been a long-standing request for C#. While F# users have had discriminated unions for years, C# developers will have to wait a bit longer.

What discriminated unions allow you to do is tell the compiler (and other tooling like your IDE) that data can be one of a range of pre-defined types.

For example, you could have a method RegisterUser() that returns either a User, a UserAlreadyExists or InvalidUsername class. These classes don’t have to inherit from each other. You want to support 3 potential return types and tell the language about this, get compiler errors if you return a 4th type, and so on.

If you have used ASP.NET Core Minimal APIs, you may have seen the Results<> and TypedResults approach to return data from your API. Using this approach, you can define which object types may be returned from your API (using Results<>). Here’s a quick example of an API that can return an Ok or Unauthorized result.

app.MapGet("/items", async Task<Results<Ok<IEnumerable<ApiItem>>, Unauthorized>>(
  [FromRoute]int storeId,
  GroceryListDb db) => {
      // ... code here
      return TypedResults.Ok(items);
  });

The Results<> type essentially a discriminated union: the return value will be one of (in this case) two types, and the ASP.NET Core Minimal API engine can use that information to return the correct type.

Digging into the source code (and removing some ASP.NET Core-specifics), the Results class with support for 3 different types looks like this:

public sealed class Results<TResult1, TResult2, TResult3>
{
    private Results(object activeResult)
    {
        Result = activeResult;
    }

    public object Result { get; }

    public static implicit operator Results<TResult1, TResult2, TResult3>(TResult1 result) => new(result);

    public static implicit operator Results<TResult1, TResult2, TResult3>(TResult2 result) => new(result);

    public static implicit operator Results<TResult1, TResult2, TResult3>(TResult3 result) => new(result);
}

It should be quite straightforward to change this into a Results class that supports 2 types, or 5.

Using implicit operators, the Results class can be instantiated from any of the types that have supported conversions.

What’s cool is that you can drop this class into your own code, and use the Results class to have, for example, a method that can return either int, bool or string, but nothing else:

Results<int, bool, string> GetData() => "Hello, world!";

If you returned a type that is not supported, the IDE (and compiler) will tell you:

Compiler warning with discriminated union

Even pattern matching is supported (if you do it on the property that holds the actual data):

var data = GetData();
var typeAsString = data.Result switch
{
    int => "int",
    bool => "bool",
    string => "string",
    _ => throw new NotImplementedException()
};

Console.WriteLine(typeAsString);

Results<int, bool, string> GetData() => "Hello, world!";

The downside however, is that when you’d change the GetData() method to return either of 4 types (instead of 3), you would not get a compilation error in the above switch expression. And let that be one of the advantages of discriminated unions: being able to get tooling support for these cases, informing you that you don’t have an exhaustive match on all types.

For ASP.NET Core Minimal APIs, the Results<> class works perfectly. It’s a discriminated union that only needs one side of the story (being able to get compiler errors when you return something you’re not supposed to). Consuming the result is part of the framework mechanics, and ideally you should never need to do an exhaustive comparison yourself.

If you’re outside ASP.NET Core Minimal APIs, you want to work with discriminated unions in your code, and you can’t wait for proper language support, there is good news for you! The OneOf package (docs) lets you work with discriminated unions, provides compiler errors when comparisons are not exhaustive, etc.

For me, the reason of writing this blog post was mainly that I wanted to show you the clever use of implicit operators in the Results<> class. I hope, however, that you got something more out of it as well: a short introduction to discriminated unions, and two alternatives (using F#, and the OneOf package) if you do want to use them in your code.

Leave a Comment

avatar

One response

  1. Avatar for falconmick
    falconmick February 19th, 2024

    Hey maybe you can help me understand actually what Discriminating Unions are… I was under the impression that for it to be a DU and not just a regular union that the options had to be tagged, kinda like how in rust enum options can have values assosiated to them or just be tags. When I look at the Results class what I see is the OneOf pattern where the T value is constrained to be IResult.. This who Union vs Tagged Union vs Discriminated Union thing has really been confusing for me, would love your feedback.. Oh and to leave this comment I had to hack this comment into the hidden message input as when I click on the text bos on firefox I cannot type!