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);
}
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
F# will give you equal operators and the other stuff for free, hence:
let id1 = (CarID 42)
let id2 = (CarID 42)
id1 = id2 // true
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;
}
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")
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)))
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
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>())
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)