Compiler did not flag an error I had made, until it did...
4.6 KiB
layout | title | date | author | categories | tags | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
post | F# Options with EF Core | 2018-07-08 17:00:00 | Daniel |
|
|
The 2.1 release of Entity Framework Core brought the ability to do value conversions. This is implemented through an abstract class, ValueConverter
, which you can implement to convert a data type. They also provided several built-in converters that you don't have to write, such as storing enum
s as strings. To use a value converter, you provide a new instance of it and attach it to a property in your model's OnModelCreating
event.
F# provides an Option<'T>
type as a way to represent a value that may or may not be present. There are many benefits to defining optional values as 'T option
rather than checking for null; you can read all about it if you'd like.
As I was working on a project, I already used Option.ofObj
to convert my possibly-null results from queries to options; at the field level, though, I was working with default values. Could I use this new feature to handle null
able columns as well? As it turns out, yes!
Here is the code for the value converter.
{% codeblock lang:fsharp %} module Conversion =
open Microsoft.FSharp.Linq.RuntimeHelpers open System open System.Linq.Expressions
let toOption<'T> = <@ Func<'T, 'T option>(fun (x : 'T) -> match box x with null -> None | _ -> Some x) @> |> LeafExpressionConverter.QuotationToExpression |> unbox<Expression<Func<'T, 'T option>>>
let fromOption<'T> = <@ Func<'T option, 'T>(fun (x : 'T option) -> match x with Some y -> y | None -> Unchecked.defaultof<'T>) @> |> LeafExpressionConverter.QuotationToExpression |> unbox<Expression<Func<'T option, 'T>>>
type OptionConverter<'T> () = inherit ValueConverter<'T option, 'T> (Conversion.fromOption, Conversion.toOption) {% endcodeblock %}
The Conversion
module contains the functions that we'll need to provide in the ValueConverter
constructor. (With the way class inheritance is coded in F#, and the way ValueConverter
wants its expressions in its constructor, this is a necessary step. I would have liked to have seen a no-argument constructor and overridable properties as an option, but I'm not complaining; this is a really great feature.) Within those functions, we make use of code quotations, then convert the quotation expressions to Linq expressions.
One other note; in the toOption
function, if we used Option.ofObj
instead of box x
, the code would not support value types. This means that things like an int option
field wouldn't be supported.
Now that we have our option converter, let's hook it into our model. In my project, each entity type has a static configureEF
function, and I call those from OnModelCreating
. Here's an abridged version of one of my entity types:
{% codeblock lang:fsharp %} [] [] [] Member = { /// ... /// E-mail format format : string option /// ... } with /// ... static member configureEF (mb : ModelBuilder) = /// ... HasColumnName statements, etc. mb.Model.FindEntityType(typeof).FindProperty("format").SetValueConverter(OptionConverter ()) |> ignore {% endcodeblock %}
This line of code finds the type within the model, the property within the type, and provides the new instance of our option converter to it. In this entity, a None
here indicates that the member uses the group's default e-mail format; Some
would indicate that they've specified which format they prefer.
That's all there is to it! Define the coverter once, and plug it in to all the optional fields; now we have nullable fields translated to options by EF Core. "Magic unicorn," indeed!
(Credits: Many thanks to Jiří Činčura for the excellent value conversion blog post and Tomas Petricek for his Stack Overflow answer on converting quotation expressions to Linq expressions.)