OneOf - Discriminated Unions for C#

OneOf - Discriminated Unions for C#

In my previous project I was using OneOf library by Harry McIntyre implementing F# like discriminated unions support for C#. They might be handy in scenarios when heterogenous data is returned from a method and varies in type from one instance to another. The data may include special cases, like happy-path result values and error cases. For example, in Web Api applications controller actions may return different object instances representing Api responses for different scenarios, which ultimately get mapped into different Http response codes:

  • 200 - OK
  • 422 - Unprocessable Entity
  • 403 - Forbidden
  • etc.

Discriminated union is a type storing one of the different values. Each value can be of different type. This type exists in the world of functional programming and not supported by the object-oriented languages like C# or Java. An F# example would look like this:

type Response =
    | GetPayment of Id : string * amount : int * currency:string * status: string
    | PaymentNotFound of reason : string
    | ServiceUnavailable

A Response instance will hold either a GetPayment, PaymentNotFound or ServiceUnavailable. Each case has a different set of fields. You can use F# match statement to handle it:

let getHttpResponse response =
    match response with
    | GetPayment payment -> OkResponse(payment)
    | PaymentNotFound notFound-> NotFoundResponse(notFound)
    | ServiceUnavailable error-> BadGatewayResponse(error)

As opposed to C#, F# compiler requires you to write each handler for each case. This makes the code more maintable.

In C# a discriminated union can be represented as a class hierarchy where the base class corresponds to the union as a whole and sub-classes correspond to the cases. For example, class GetPayment: Response{}. Then you can use a switch statement (starting from C# 7)

switch(response)
{
    case GetPayment gp:
        break;
    default:
        break;
}

This code will continue to compile even if you miss some cases. The issues may come up in runtime if a case was missed or the library containing the hierarchy was extended with new types.

We can get similar behaviour using OneOf. You can define a custom struct OneOf<T0, …, Tn> where you list the possible types as generic arguments.

Using OneOf

When using OneOf in C# it helps to have compile-time checks and more explicit interfaces. For example, in Web Api you would have to define IActionResult or Object without OneOf in order to accommodate the scenario above. The library is very handy if you want to generate documentation or an SDK for the Api.

OneOf implicitly casts from values of their generic parameter types. For example,

var getPayment = new GetPayment {Id = "pay_id", Amount = 100, Currency = "USD", Status = "Authorized"};
OneOf<GetPayment, PaymentNotFound, ServiceUnavailable> response = getPayment;

//You can then access the value .AsT0
Console.Writeline(response.AsT0.Id); // get payment Id

It works great when you use OneOf as a return type. For example,

public async Task<OneOf<GetPayment, PaymentNotFound, ServiceUnavailable>> GetPaymentAsync(string id)
        {
            if(string.IsNullOrEmpty(id))
                return new PaymentNotFound();
            
            GetPayment payment = null;
            try
            {
                payment = await service.GetPaymentAsync(id);
            }
            catch(Exception)
            {
                return new ServiceUnavailable();
            }
            return payment;
        }

You can use TOut Match(Func<T0, TOut> f0, ... Func<Tn,TOut> fn) to perform an action based on the actual value similar to C# switch and return a value:


public async Task<HttpResponseMessage> GetPaymentActionAsync(string id)
{
 var response = await GetPaymentAsync("pay_id");
 
 HttpResponseMessage responseMessage = 
         response.Match(
                   getPayment => GetResponse(HttpStatusCode.OK, getPayment),
                   notFound => GetResponse(HttpStatusCode.NotFound, null),
                   unavailable => GetResponse(HttpStatusCode.BadGateway, null)
                   );
                                        
 return responseMessage;
 }

private HttpResponseMessage GetResponse<T>(HttpStatusCode code, T response)
{
    return Request.CreateResponse(code, response);
}

This has a major advantage over a switch statement, as it

  • requires every parameter to be handled
  • No fallback - if you add another generic parameter, you MUST update all the calling code to handle your changes. In brown-field code-bases, this is incredibly useful, as the default handler is often a runtime throw NotImplementedException, or behaviour that wouldn't suit the new result type.

There is also a Switch() method, for when you aren't returning a value.

Summary

OneOf is a nice C# implementation of distinctive unions, which are an alternative to polymorphism when you want to have a method with guaranteed behaviour-per-type (i.e. adding an abstract method on a base type, and then implementing that method in each type). It improves the code base by adding compile-time case values checks and maks a clearer Api contract.