In this post I demostrate a practical use of Discriminated Union data structure / pattern in C# code.

I also show the alternative design solutions and explain how they are inferior.

The Need for a ‘One-Of-Many’ Type

Sometimes we want to model a concept that can take different forms at runtime, but should still be treated as one type.

This is how it looks in UML:

AbstractType can be one of many concrete types

We want to be able to pass objects as instances of AbstractType, but still have access to members of concrete types.

Seems pretty basic. Can’t we already do this in C#? Unfortunately, we can’t do it cleanly, as I’ll show later.

A Practical Example

Let’s look at a concrete example of a domain where using discriminated unions makes sense.

Suppose there’s a communication library where a class hierarchy of response types looked like this:

Class hierarchy that illustrates the need for discriminated unions

This is what I wanted the library API to look like:

public interface Client{
    ReadResult Read(Request r);
}

Now the question is - how can the ReadResult type represent all the possible variations of response?

Naive Solutions - Before Discriminated Unions

Inheritance

The first thing that might come to mind in this case is using inheritance.

Unfortunately this is a bad solution because the concrete types represent completely different things that don’t share any data or behavior.

Such API would break LSP and require the client to manually type-check and upcast responses to a concrete type.

This obviously is inconvenient and unsafe.

Composition

Using composition would mean combining data from different types into one “container” type and using a special “tag” property to differentiate between types, for example:

class ReadResult{
    // The `type tag`
    public ResultType ResultType {get;}

    // properties from `ReadSuccess`
    public byte[] Data { get; }

    // properties from `IOError`
    public ErrorCode Error { get; }
}

Here ResultType basically signifies which properties of a specific instance are valid to use. In a successful response you can the Data property is applicable, while in case of an error the Error property makes sense.

In practice this is even more complicated, because there are different types of errors, each with it’s own types of data.

This solution is not safe because there’s no compile-time guarantee that the correct properties will always be used. Also, the container object can be made inconsistent very easily, or should contain logic to guard against this. This is is extra code we shouldn’t have to write.

The Solution with Discriminated Unions

Recently I got interested in F# and was amazed that there are languages that actually handle this modelling scenario. They have Discriminated Unions as a language feature.

After a bit more research I implemented something similar in C#.

This is how you work with an API that returns a Discriminated Union:

ReadResult result = client.Read(request);
result.Match(success => {
    DoSomethingWithData(success.Data);
}, failure => {
    HandleFailure(failure);
});

Discriminated Library (C#)

I liked how the solution worked, so I decided to create a small library to re-use in future projects.

The library is called Discriminated and it’s available at nuget:

PM> Install discriminated

After installing the library, I simply inherit it’s Union<T> type by ReadResult and use it in the API.

class ReadResult: Union<ReadSuccess, IOError>{

}

From Union<T1,T2>, ReadResult inherits the Match method, which allows/requires the caller to pass callbacks for all possible cases of the union. There are two possible cases in this example, so Match takes either two Func objects or two Action objects.

Benefits of Discriminated Unions

The best thing about this approach is it compile-time type safety. You don’t need to type-check and up-cast.

It also guarantees all Union cases will handled by the client.

Finally, it’s the best approach from modelling purity perspective. I no longer have to force things into an inheritance hierarcy when they don’t actually have any polymorhpic behavior.