DEV Community

Cover image for Using single case union types for entity IDs in F# and making it work with Dapper
Jakob Christensen
Jakob Christensen

Posted on • Originally published at leruplund.dk on

Using single case union types for entity IDs in F# and making it work with Dapper

In C# it is a common and popular pattern to use strongly typed entity IDs instead of using integers or the likes for IDs on your entities. Strongly typed entity IDs is a great help when trying to prevent you from mixing an Order ID with an OrderLine ID.

Andrew Lock recently wrote a series on the topic that you should go read now.

A strongly typed entity ID may look something like this (the example used by Andrew Lock):

public readonly struct OrderId : IComparable<OrderId>, IEquatable<OrderId>
{
    public int Value { get; }

    public OrderId(int value)
    {
        Value = value;
    }

    public bool Equals(OrderId other) => this.Value.Equals(other.Value);
    public int CompareTo(OrderId other) => Value.CompareTo(other.Value);

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        return obj is OrderId other && Equals(other);
    }

    public override int GetHashCode() => Value.GetHashCode();
    public override string ToString() => Value.ToString();

    public static bool operator ==(OrderId a, OrderId b) => a.CompareTo(b) == 0;
    public static bool operator !=(OrderId a, OrderId b) => !(a == b);
}
Enter fullscreen mode Exit fullscreen mode

That is a lot of code to write for every ID in your domain model and you should create a code snippet in Visual Studio to do it. However, as with many things you get this more or less for free in F# with single case union types. All it takes is this:

type CarID = CarID of int
Enter fullscreen mode Exit fullscreen mode

F# will give you equal operators and the other stuff for free, hence:

let id1 = (CarID 42)
let id2 = (CarID 42)

id1 = id2 // true
Enter fullscreen mode Exit fullscreen mode

As Scott Wlaschin points out on the legendary F# for fun and profit you can use single case union types for anything, not just IDs:

module Domain =
    type CarID = CarID of int
    type CarMake = CarMake of string
    type Model = Model of string

    type Car = {
        CarID: CarID;
        Make: CarMake;
        Year: int;
        Model: Model;
    }
Enter fullscreen mode Exit fullscreen mode

I am not saying this is always a good idea but don’t take it for any more than an example.

What happens if you try to read a Car from a database with Dapper?

Single case union types and Dapper

Dapper uses reflection to create instances of your entity types either by calling the appropriate constructor or by setting properties. For F# record types you cannot set the properties so Dapper tries to call the constructor when querying the database:

use cn = getConnection ()
let cars = cn.Query<Car>("SELECT CarID, Make, Year, Model FROM Car")
Enter fullscreen mode Exit fullscreen mode

This will fail because Dapper tries to find a constructor on OrderLine with the signature (int * string * int * string).

To help Dapper convert from int to CarID, from string to Make and so on you can register a type handler with Dapper. A type handler is a class that inherits SqlMapper.TypeHandler<>. For reading CarID from the database the type handler would look like this.

type CarIDTypeHandler() =
        inherit SqlMapper.TypeHandler<CarID>()

        override x.SetValue(parameter: IDbDataParameter, value: CarID) =
            ()

        override x.Parse(value: obj) = 
            (CarID (Convert.ToInt32(value)))
Enter fullscreen mode Exit fullscreen mode

You register the type handler with Dapper with SqlMapper.AddTypeHandler(typeof<CarID>, new CarIDTypeHandler()). It gets boring very quickly to write type handlers for every single single case union type you come up with (sorry about that strange sentence) and fortunately there is a way to write a generic type handler for single case union types using reflection.

type SingleCaseUnionTypeHandler<'T>() =
        inherit SqlMapper.TypeHandler<'T>()

        override x.SetValue(parameter: IDbDataParameter, value: 'T) =
            ()

        override x.Parse(value: obj) =
            let cases = FSharpType.GetUnionCases(typedefof<'T>)
            FSharpValue.MakeUnion(cases.[0], [| value |]) :?> 'T
Enter fullscreen mode Exit fullscreen mode

The last line makes use of FSharpValue.MakeUnion to create a union. It assumes that the union type 'T has one and only one case when using cases.[0]. I have not found a way to restrict the generic type parameter to unions.

Now you can register the type handler for all single case union types:

SqlMapper.AddTypeHandler(typeof<CarID>, new SingleCaseUnionTypeHandler<CarID>())
SqlMapper.AddTypeHandler(typeof<CarMake>, new SingleCaseUnionTypeHandler<CarMake>())
SqlMapper.AddTypeHandler(typeof<Model>, new SingleCaseUnionTypeHandler<Model>())
Enter fullscreen mode Exit fullscreen mode

Closing

Above we have seen how we can easily use single case union types for type safe handling of entity IDs and other values to prevent using them interchangeably. We have also created a generic Dapper type handler for single case union types.

The generic type handler is only suitable for cases where you do not need extra logic or validation for creating your single union value types and you should also be careful when it comes to performance issues with the reflection in case you need to handle large amounts of instances.

Top comments (0)